gantt-renderer 0.2.0 → 0.4.0
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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/index.d.mts +175 -84
- package/dist/index.mjs +1035 -280
- package/dist/index.mjs.map +1 -1
- package/dist/styles/gantt.css +17 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/lib/validation/schemas.ts
|
|
3
3
|
const LinkTypeSchema = z.enum([
|
|
4
4
|
"FS",
|
|
5
5
|
"SS",
|
|
6
6
|
"FF",
|
|
7
7
|
"SF"
|
|
8
8
|
]);
|
|
9
|
-
const
|
|
9
|
+
const TaskKindSchema = z.enum([
|
|
10
10
|
"task",
|
|
11
11
|
"project",
|
|
12
12
|
"milestone"
|
|
@@ -14,45 +14,62 @@ const TaskTypeSchema = z.enum([
|
|
|
14
14
|
const SpecialDayKindSchema = z.enum(["holiday", "custom"]);
|
|
15
15
|
const SpecialDaySchema = z.object({
|
|
16
16
|
/** ISO date: YYYY-MM-DD */
|
|
17
|
-
date: z.string().regex(/^\d{4}-\d{2}-\d{2}
|
|
17
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "Expected YYYY-MM-DD"),
|
|
18
18
|
kind: SpecialDayKindSchema,
|
|
19
19
|
label: z.string().min(1).optional(),
|
|
20
20
|
className: z.string().min(1).optional()
|
|
21
21
|
});
|
|
22
|
-
const
|
|
22
|
+
const taskBase = {
|
|
23
23
|
/** Unique positive integer identifier for the task. */
|
|
24
24
|
id: z.number().int().positive(),
|
|
25
25
|
/** Display name / label of the task. */
|
|
26
26
|
text: z.string().min(1),
|
|
27
27
|
/** ISO date: YYYY-MM-DD */
|
|
28
|
-
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}
|
|
29
|
-
/** Duration in hours; 0 = milestone */
|
|
30
|
-
durationHours: z.number().int().min(0),
|
|
28
|
+
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "Expected YYYY-MM-DD"),
|
|
31
29
|
/** Optional id of the parent task. When set, this task is a child in the hierarchy. */
|
|
32
30
|
parent: z.number().int().positive().optional(),
|
|
33
|
-
/**
|
|
31
|
+
/** Optional CSS color value for the task bar. Overrides the default color assignment. */
|
|
32
|
+
color: z.string().optional(),
|
|
33
|
+
/** When `true`, the task bar cannot be dragged or resized. */
|
|
34
|
+
readonly: z.boolean().optional(),
|
|
35
|
+
/** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
|
|
36
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
37
|
+
};
|
|
38
|
+
/** @internal */
|
|
39
|
+
const TaskLeafSchema = z.object({
|
|
40
|
+
...taskBase,
|
|
41
|
+
kind: z.literal("task"),
|
|
42
|
+
/** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
|
|
43
|
+
durationHours: z.number().int().positive(),
|
|
44
|
+
/** 0–100 completion percentage (integer). */
|
|
45
|
+
percentComplete: z.number().int().min(0).max(100).default(0)
|
|
46
|
+
});
|
|
47
|
+
/** @internal */
|
|
48
|
+
const TaskProjectSchema = z.object({
|
|
49
|
+
...taskBase,
|
|
50
|
+
kind: z.literal("project"),
|
|
51
|
+
/** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
|
|
52
|
+
durationHours: z.number().int().positive(),
|
|
53
|
+
/** 0–100 completion percentage (integer). */
|
|
34
54
|
percentComplete: z.number().int().min(0).max(100).default(0),
|
|
35
55
|
/**
|
|
36
|
-
* Task type: `'task'`, `'project'`, or `'milestone'`.
|
|
37
|
-
*
|
|
38
|
-
* - `'task'` — A regular task with a colored bar.
|
|
39
|
-
* - `'project'` — A summary/group row with a colored bar.
|
|
40
|
-
* - `'milestone'` — A zero-duration marker rendered as a diamond.
|
|
41
|
-
*
|
|
42
|
-
* @default 'task'
|
|
43
|
-
*/
|
|
44
|
-
type: TaskTypeSchema.default("task"),
|
|
45
|
-
/**
|
|
46
56
|
* Initial expanded state for tree hierarchy.
|
|
47
57
|
* When `false`, children of this task are hidden on initial render.
|
|
48
|
-
* Only relevant for tasks with child tasks.
|
|
49
58
|
*
|
|
50
59
|
* @default true
|
|
51
60
|
*/
|
|
52
|
-
open: z.boolean().default(true)
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
open: z.boolean().default(true)
|
|
62
|
+
});
|
|
63
|
+
/** @internal */
|
|
64
|
+
const TaskMilestoneSchema = z.object({
|
|
65
|
+
...taskBase,
|
|
66
|
+
kind: z.literal("milestone")
|
|
55
67
|
});
|
|
68
|
+
const TaskSchema = z.discriminatedUnion("kind", [
|
|
69
|
+
TaskLeafSchema,
|
|
70
|
+
TaskProjectSchema,
|
|
71
|
+
TaskMilestoneSchema
|
|
72
|
+
]);
|
|
56
73
|
const LinkSchema = z.object({
|
|
57
74
|
/** Unique positive integer identifier for the dependency link. */
|
|
58
75
|
id: z.number().int().positive(),
|
|
@@ -70,13 +87,66 @@ const LinkSchema = z.object({
|
|
|
70
87
|
*
|
|
71
88
|
* @default 'FS'
|
|
72
89
|
*/
|
|
73
|
-
type: LinkTypeSchema.default("FS")
|
|
90
|
+
type: LinkTypeSchema.default("FS"),
|
|
91
|
+
/** When `true`, the link cannot be modified or deleted through the UI. */
|
|
92
|
+
readonly: z.boolean().optional(),
|
|
93
|
+
/** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
|
|
94
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
95
|
+
}).refine((l) => l.source !== l.target, {
|
|
96
|
+
message: "A link cannot connect a task to itself",
|
|
97
|
+
path: ["target"]
|
|
74
98
|
});
|
|
75
99
|
const GanttInputSchema = z.object({
|
|
76
100
|
/** Array of task objects. At least one task is required. */
|
|
77
101
|
tasks: z.array(TaskSchema).min(1),
|
|
78
102
|
/** Optional array of dependency link objects. Defaults to empty array. */
|
|
79
103
|
links: z.array(LinkSchema).default([])
|
|
104
|
+
}).superRefine((data, ctx) => {
|
|
105
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
106
|
+
for (let i = 0; i < data.tasks.length; i++) {
|
|
107
|
+
const task = data.tasks[i];
|
|
108
|
+
if (task !== void 0) {
|
|
109
|
+
if (taskIds.has(task.id)) ctx.addIssue({
|
|
110
|
+
code: "custom",
|
|
111
|
+
message: `Duplicate task id: ${task.id}`,
|
|
112
|
+
path: [
|
|
113
|
+
"tasks",
|
|
114
|
+
i,
|
|
115
|
+
"id"
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
taskIds.add(task.id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const linkIds = /* @__PURE__ */ new Set();
|
|
122
|
+
for (let i = 0; i < data.links.length; i++) {
|
|
123
|
+
const link = data.links[i];
|
|
124
|
+
if (link !== void 0) {
|
|
125
|
+
if (linkIds.has(link.id)) ctx.addIssue({
|
|
126
|
+
code: "custom",
|
|
127
|
+
message: `Duplicate link id: ${link.id}`,
|
|
128
|
+
path: [
|
|
129
|
+
"links",
|
|
130
|
+
i,
|
|
131
|
+
"id"
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
linkIds.add(link.id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const pairKeys = /* @__PURE__ */ new Set();
|
|
138
|
+
for (let i = 0; i < data.links.length; i++) {
|
|
139
|
+
const link = data.links[i];
|
|
140
|
+
if (link !== void 0) {
|
|
141
|
+
const key = `${link.source}:${link.target}`;
|
|
142
|
+
if (pairKeys.has(key)) ctx.addIssue({
|
|
143
|
+
code: "custom",
|
|
144
|
+
message: `Duplicate link pair: source=${link.source} target=${link.target}`,
|
|
145
|
+
path: ["links", i]
|
|
146
|
+
});
|
|
147
|
+
pairKeys.add(key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
80
150
|
});
|
|
81
151
|
/**
|
|
82
152
|
* Parses raw external data.
|
|
@@ -88,18 +158,8 @@ const GanttInputSchema = z.object({
|
|
|
88
158
|
function parseGanttInput(raw) {
|
|
89
159
|
return GanttInputSchema.parse(raw);
|
|
90
160
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Parses without throwing; returns `null` on validation failure.
|
|
93
|
-
*
|
|
94
|
-
* @param raw - The unvalidated input from the consumer.
|
|
95
|
-
* @returns The parsed {@link GanttInput} or `null` when the input is invalid.
|
|
96
|
-
*/
|
|
97
|
-
function safeParseGanttInput(raw) {
|
|
98
|
-
const result = GanttInputSchema.safeParse(raw);
|
|
99
|
-
return result.success ? result.data : null;
|
|
100
|
-
}
|
|
101
161
|
//#endregion
|
|
102
|
-
//#region src/
|
|
162
|
+
//#region src/lib/errors.ts
|
|
103
163
|
/**
|
|
104
164
|
* Domain-specific error with a machine-readable {@link GanttErrorCode}.
|
|
105
165
|
*/
|
|
@@ -116,23 +176,67 @@ var GanttError = class extends Error {
|
|
|
116
176
|
}
|
|
117
177
|
};
|
|
118
178
|
//#endregion
|
|
119
|
-
//#region src/
|
|
179
|
+
//#region src/lib/domain/tree.ts
|
|
180
|
+
function detectParentCycles(tasks) {
|
|
181
|
+
const adj = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const task of tasks) {
|
|
183
|
+
adj.set(task.id, []);
|
|
184
|
+
if (task.parent !== void 0) {
|
|
185
|
+
const parents = adj.get(task.parent);
|
|
186
|
+
if (parents !== void 0) parents.push(task.id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
190
|
+
const color = /* @__PURE__ */ new Map();
|
|
191
|
+
const parent = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const id of adj.keys()) color.set(id, WHITE);
|
|
193
|
+
const dfs = (u) => {
|
|
194
|
+
color.set(u, GRAY);
|
|
195
|
+
for (const v of adj.get(u) ?? []) {
|
|
196
|
+
const vc = color.get(v) ?? WHITE;
|
|
197
|
+
if (vc === GRAY) {
|
|
198
|
+
const path = [v, u];
|
|
199
|
+
let cur = u;
|
|
200
|
+
while (cur !== v) {
|
|
201
|
+
const p = parent.get(cur);
|
|
202
|
+
if (p === void 0) break;
|
|
203
|
+
path.push(p);
|
|
204
|
+
cur = p;
|
|
205
|
+
}
|
|
206
|
+
throw new GanttError("PARENT_CYCLE", `Parent cycle detected: ${[...path].reverse().join(" -> ")}`);
|
|
207
|
+
}
|
|
208
|
+
if (vc === WHITE) {
|
|
209
|
+
parent.set(v, u);
|
|
210
|
+
dfs(v);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
color.set(u, BLACK);
|
|
214
|
+
};
|
|
215
|
+
for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
|
|
216
|
+
}
|
|
120
217
|
/**
|
|
121
218
|
* Builds a typed tree from a flat task array.
|
|
122
219
|
* Order of tasks[] is irrelevant — parents need not precede children.
|
|
123
220
|
*
|
|
124
221
|
* @param tasks - The flat array of tasks to convert into a tree.
|
|
125
222
|
* @returns Root-level {@link TaskNode} instances with populated `children`.
|
|
126
|
-
* @throws {GanttError} When a task references a `parent` id that does not exist
|
|
223
|
+
* @throws {GanttError} When a task references a `parent` id that does not exist,
|
|
224
|
+
* when a parent cycle is detected, or when a `parent` points to a
|
|
225
|
+
* milestone or leaf task.
|
|
127
226
|
*/
|
|
128
227
|
function buildTaskTree(tasks) {
|
|
129
228
|
const map = /* @__PURE__ */ new Map();
|
|
130
229
|
const roots = [];
|
|
230
|
+
for (const task of tasks) if (task.parent !== void 0) {
|
|
231
|
+
const parentTask = tasks.find((t) => t.id === task.parent);
|
|
232
|
+
if (parentTask !== void 0 && (parentTask.kind === "milestone" || parentTask.kind === "task")) throw new GanttError("PARENT_REFERENCE", `Task id=${task.id} cannot have parent id=${task.parent} of kind '${parentTask.kind}'`);
|
|
233
|
+
}
|
|
131
234
|
for (const task of tasks) map.set(task.id, {
|
|
132
235
|
...task,
|
|
133
236
|
children: [],
|
|
134
237
|
depth: 0
|
|
135
238
|
});
|
|
239
|
+
detectParentCycles(tasks);
|
|
136
240
|
for (const task of tasks) {
|
|
137
241
|
const node = map.get(task.id);
|
|
138
242
|
if (node === void 0) continue;
|
|
@@ -177,7 +281,7 @@ function isParent(node) {
|
|
|
177
281
|
return node.children.length > 0;
|
|
178
282
|
}
|
|
179
283
|
//#endregion
|
|
180
|
-
//#region src/
|
|
284
|
+
//#region src/lib/domain/dependencies.ts
|
|
181
285
|
/**
|
|
182
286
|
* Detects circular dependencies in the link graph using DFS tri-colour marking.
|
|
183
287
|
*
|
|
@@ -221,21 +325,34 @@ function detectCycles(tasks, links) {
|
|
|
221
325
|
for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
|
|
222
326
|
}
|
|
223
327
|
/**
|
|
224
|
-
* Validates that every link references existing task IDs
|
|
328
|
+
* Validates that every link references existing task IDs and that no
|
|
329
|
+
* duplicate (source, target) pairs exist.
|
|
225
330
|
*
|
|
226
331
|
* @param tasks - The task list (used as the reference set of valid IDs).
|
|
227
332
|
* @param links - The dependency links to validate.
|
|
228
|
-
* @throws {GanttError} When any link references a non-existent source or target task
|
|
333
|
+
* @throws {GanttError} When any link references a non-existent source or target task,
|
|
334
|
+
* when a non-FS link connects to/from a milestone, or when duplicate
|
|
335
|
+
* (source, target) pairs exist.
|
|
229
336
|
*/
|
|
230
337
|
function validateLinkRefs(tasks, links) {
|
|
231
338
|
const ids = new Set(tasks.map((t) => t.id));
|
|
339
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
340
|
+
const pairKeys = /* @__PURE__ */ new Set();
|
|
232
341
|
for (const link of links) {
|
|
233
342
|
if (!ids.has(link.source)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: source=${link.source} not found`);
|
|
234
343
|
if (!ids.has(link.target)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: target=${link.target} not found`);
|
|
344
|
+
const pairKey = `${link.source}:${link.target}`;
|
|
345
|
+
if (pairKeys.has(pairKey)) throw new GanttError("DUPLICATE_LINK_PAIR", `Link id=${link.id}: duplicate pair source=${link.source} target=${link.target}`);
|
|
346
|
+
pairKeys.add(pairKey);
|
|
347
|
+
if (link.type !== "FS") {
|
|
348
|
+
const sourceTask = taskById.get(link.source);
|
|
349
|
+
const targetTask = taskById.get(link.target);
|
|
350
|
+
if (sourceTask?.kind === "milestone" || targetTask?.kind === "milestone") throw new GanttError("MILESTONE_LINK_TYPE", `Link id=${link.id}: non-FS type '${link.type}' not allowed when connected to a milestone`);
|
|
351
|
+
}
|
|
235
352
|
}
|
|
236
353
|
}
|
|
237
354
|
//#endregion
|
|
238
|
-
//#region src/
|
|
355
|
+
//#region src/lib/locale.ts
|
|
239
356
|
const WEEK_START_REGION = {
|
|
240
357
|
US: 0,
|
|
241
358
|
CA: 0,
|
|
@@ -517,7 +634,7 @@ function formatLabel(template, arg) {
|
|
|
517
634
|
return template.replaceAll("{0}", arg);
|
|
518
635
|
}
|
|
519
636
|
//#endregion
|
|
520
|
-
//#region src/
|
|
637
|
+
//#region src/lib/domain/dateMath.ts
|
|
521
638
|
/**
|
|
522
639
|
* Parses `YYYY-MM-DD` → UTC midnight `Date`.
|
|
523
640
|
*
|
|
@@ -657,7 +774,7 @@ function formatDisplayDate(dateStr, locale) {
|
|
|
657
774
|
});
|
|
658
775
|
}
|
|
659
776
|
//#endregion
|
|
660
|
-
//#region src/
|
|
777
|
+
//#region src/lib/timeline/scale.ts
|
|
661
778
|
const H = 36e5;
|
|
662
779
|
const D = 864e5;
|
|
663
780
|
const SCALE_CONFIGS = {
|
|
@@ -740,7 +857,7 @@ function nextScaleBoundary(date, scale) {
|
|
|
740
857
|
}
|
|
741
858
|
}
|
|
742
859
|
//#endregion
|
|
743
|
-
//#region src/
|
|
860
|
+
//#region src/lib/timeline/pixelMapper.ts
|
|
744
861
|
/**
|
|
745
862
|
* Creates a stateless pixel mapper for the given scale and viewport start.
|
|
746
863
|
* All conversions are O(1) arithmetic — safe to call in tight loops.
|
|
@@ -773,7 +890,7 @@ function createPixelMapper(scale, viewportStart) {
|
|
|
773
890
|
};
|
|
774
891
|
}
|
|
775
892
|
//#endregion
|
|
776
|
-
//#region src/
|
|
893
|
+
//#region src/lib/timeline/layoutEngine.ts
|
|
777
894
|
const DENSITY = {
|
|
778
895
|
rowHeight: 44,
|
|
779
896
|
barHeight: 28,
|
|
@@ -802,8 +919,7 @@ function computeLayout(rows, mapper) {
|
|
|
802
919
|
const x = mapper.toX(start);
|
|
803
920
|
const y = i * ROW_HEIGHT + BAR_Y_OFFSET;
|
|
804
921
|
const centerY = i * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
805
|
-
|
|
806
|
-
if (type === "milestone") {
|
|
922
|
+
if (task.kind === "milestone") {
|
|
807
923
|
result.set(task.id, {
|
|
808
924
|
taskId: task.id,
|
|
809
925
|
x,
|
|
@@ -811,7 +927,7 @@ function computeLayout(rows, mapper) {
|
|
|
811
927
|
width: 0,
|
|
812
928
|
height: BAR_HEIGHT,
|
|
813
929
|
progressWidth: 0,
|
|
814
|
-
|
|
930
|
+
kind: "milestone",
|
|
815
931
|
rowIndex: i,
|
|
816
932
|
centerX: x,
|
|
817
933
|
centerY
|
|
@@ -827,7 +943,7 @@ function computeLayout(rows, mapper) {
|
|
|
827
943
|
width,
|
|
828
944
|
height: BAR_HEIGHT,
|
|
829
945
|
progressWidth,
|
|
830
|
-
|
|
946
|
+
kind: task.kind,
|
|
831
947
|
rowIndex: i,
|
|
832
948
|
centerX: x + width / 2,
|
|
833
949
|
centerY
|
|
@@ -860,99 +976,245 @@ function deriveViewport(tasks, paddingHours = 48) {
|
|
|
860
976
|
let maxMs = -Infinity;
|
|
861
977
|
for (const task of tasks) {
|
|
862
978
|
const start = parseDate(task.startDate);
|
|
863
|
-
const end = addHours(start, task.durationHours);
|
|
864
979
|
if (start.getTime() < minMs) minMs = start.getTime();
|
|
865
|
-
if (
|
|
980
|
+
if (task.kind !== "milestone") {
|
|
981
|
+
const end = addHours(start, task.durationHours);
|
|
982
|
+
if (end.getTime() > maxMs) maxMs = end.getTime();
|
|
983
|
+
} else if (start.getTime() > maxMs) maxMs = start.getTime();
|
|
866
984
|
}
|
|
867
985
|
return [addHours(new Date(minMs), -paddingHours), addHours(new Date(maxMs), paddingHours)];
|
|
868
986
|
}
|
|
869
987
|
//#endregion
|
|
870
|
-
//#region src/
|
|
871
|
-
|
|
988
|
+
//#region src/lib/rendering/linkRouter.ts
|
|
989
|
+
/** px gap before/after bar for routing clearance and arrow approach */
|
|
990
|
+
const TURN_MARGIN = 24;
|
|
991
|
+
/** px vertical offset below the bar row for same-row loop detours */
|
|
992
|
+
const SAME_ROW_DETOUR = 24;
|
|
993
|
+
/** Segments shorter than this (px) are collapsed before stroking */
|
|
994
|
+
const STUB_THRESHOLD = 2;
|
|
995
|
+
/**
|
|
996
|
+
* Removes consecutive points whose Euclidean distance is below {@link STUB_THRESHOLD}.
|
|
997
|
+
* The first point is always kept. This prevents near‑zero‑length segments from
|
|
998
|
+
* appearing as visible stubs near the arrowhead.
|
|
999
|
+
*
|
|
1000
|
+
* @param points - The ordered vertex list.
|
|
1001
|
+
* @returns A filtered copy, guaranteed to contain at least the first point.
|
|
1002
|
+
*/
|
|
1003
|
+
function collapseStubs(points) {
|
|
1004
|
+
const out = [];
|
|
1005
|
+
for (const pt of points) {
|
|
1006
|
+
const last = out.at(-1);
|
|
1007
|
+
if (last === void 0 || Math.hypot(pt.x - last.x, pt.y - last.y) >= STUB_THRESHOLD) out.push(pt);
|
|
1008
|
+
}
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* True when the dependency arrow enters the target on its **left** edge (FS, SS).
|
|
1013
|
+
* False when the arrow enters on the target's **right** edge (FF, SF).
|
|
1014
|
+
*
|
|
1015
|
+
* The arrowhead uses SVG `orient="auto"` so it rotates to match the direction
|
|
1016
|
+
* of the **last** path segment. Therefore:
|
|
1017
|
+
*
|
|
1018
|
+
* - Left-entry → last segment must travel **RIGHT** (penultimate.x < tx).
|
|
1019
|
+
* - Right-entry → last segment must travel **LEFT** (penultimate.x > tx).
|
|
1020
|
+
*
|
|
1021
|
+
* @param type - The link type.
|
|
1022
|
+
* @returns `true` for FS / SS, `false` for FF / SF.
|
|
1023
|
+
*/
|
|
1024
|
+
function isLeftEntry(type) {
|
|
1025
|
+
return type === "FS" || type === "SS";
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* True when the link exits the source bar on its **right** edge (FS, FF).
|
|
1029
|
+
* False when it exits on the **left** edge (SS, SF).
|
|
1030
|
+
*
|
|
1031
|
+
* The first step after the source anchor should move **away** from the bar,
|
|
1032
|
+
* **not** into it. This means:
|
|
1033
|
+
*
|
|
1034
|
+
* - Exit‑right → first horizontal segment goes RIGHT (+TURN_MARGIN).
|
|
1035
|
+
* - Exit‑left → first horizontal segment goes LEFT (-TURN_MARGIN).
|
|
1036
|
+
*
|
|
1037
|
+
* @param type - The link type.
|
|
1038
|
+
* @returns `true` for FS / FF, `false` for SS / SF.
|
|
1039
|
+
*/
|
|
1040
|
+
function isExitRight(type) {
|
|
1041
|
+
return type === "FS" || type === "FF";
|
|
1042
|
+
}
|
|
872
1043
|
/**
|
|
873
|
-
*
|
|
1044
|
+
* Computes anchor points for the given link type.
|
|
1045
|
+
*
|
|
1046
|
+
* | Type | Source anchor (`sx`) | Target anchor (`tx`) |
|
|
1047
|
+
* |------|-----------------------------|-------------------------------|
|
|
1048
|
+
* | FS | right edge of source | left edge of target |
|
|
1049
|
+
* | SS | left edge of source | left edge of target |
|
|
1050
|
+
* | FF | right edge of source | right edge of target |
|
|
1051
|
+
* | SF | left edge of source | right edge of target |
|
|
874
1052
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
* SS = source.start → target.start
|
|
878
|
-
* FF = source.finish → target.finish
|
|
879
|
-
* SF = source.start → target.finish
|
|
1053
|
+
* Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
|
|
1054
|
+
* ± width for zero‑width milestones.
|
|
880
1055
|
*
|
|
881
1056
|
* @param type - The link type determining start/end anchor points.
|
|
882
|
-
* @param src
|
|
883
|
-
* @param tgt
|
|
884
|
-
* @returns
|
|
1057
|
+
* @param src - The source bar layout.
|
|
1058
|
+
* @param tgt - The target bar layout.
|
|
1059
|
+
* @returns Anchor x coordinates `{sx, tx}`.
|
|
1060
|
+
* @throws {Error} if the link type is not handled (exhaustiveness guard).
|
|
885
1061
|
*/
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
const
|
|
1062
|
+
function getAnchors(type, src, tgt) {
|
|
1063
|
+
const srcRight = src.kind === "milestone" ? src.x + MILESTONE_HALF : src.x + src.width;
|
|
1064
|
+
const srcLeft = src.kind === "milestone" ? src.x - MILESTONE_HALF : src.x;
|
|
1065
|
+
const tgtRight = tgt.kind === "milestone" ? tgt.x + MILESTONE_HALF : tgt.x + tgt.width;
|
|
1066
|
+
const tgtLeft = tgt.kind === "milestone" ? tgt.x - MILESTONE_HALF : tgt.x;
|
|
890
1067
|
switch (type) {
|
|
891
|
-
case "FS":
|
|
892
|
-
sx
|
|
893
|
-
tx
|
|
894
|
-
|
|
895
|
-
case "SS":
|
|
896
|
-
sx
|
|
897
|
-
tx
|
|
898
|
-
|
|
899
|
-
case "FF":
|
|
900
|
-
sx
|
|
901
|
-
tx
|
|
902
|
-
|
|
903
|
-
case "SF":
|
|
904
|
-
sx
|
|
905
|
-
tx
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1068
|
+
case "FS": return {
|
|
1069
|
+
sx: srcRight,
|
|
1070
|
+
tx: tgtLeft
|
|
1071
|
+
};
|
|
1072
|
+
case "SS": return {
|
|
1073
|
+
sx: srcLeft,
|
|
1074
|
+
tx: tgtLeft
|
|
1075
|
+
};
|
|
1076
|
+
case "FF": return {
|
|
1077
|
+
sx: srcRight,
|
|
1078
|
+
tx: tgtRight
|
|
1079
|
+
};
|
|
1080
|
+
case "SF": return {
|
|
1081
|
+
sx: srcLeft,
|
|
1082
|
+
tx: tgtRight
|
|
1083
|
+
};
|
|
1084
|
+
default: throw new Error(`Unhandled link type: ${String(type)}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Routes a link whose source and target rows are within 1 px of each other.
|
|
1089
|
+
*
|
|
1090
|
+
* **Direct‑line optimisation**
|
|
1091
|
+
* A plain horizontal segment is only used when it is non‑degenerate (`sx ≠ tx`)
|
|
1092
|
+
* AND the arrowhead direction is visually correct:
|
|
1093
|
+
*
|
|
1094
|
+
* | Entry side | Condition | Arrow direction |
|
|
1095
|
+
* |-----------|-----------|----------------|
|
|
1096
|
+
* | left | `sx < tx` | → RIGHT ✓ |
|
|
1097
|
+
* | right | `sx > tx` | ← LEFT ✓ |
|
|
1098
|
+
*
|
|
1099
|
+
* Otherwise a 6‑vertex detour is drawn so that the last segment approaches
|
|
1100
|
+
* the target from the correct side. By default the detour goes **below** the
|
|
1101
|
+
* bars; pass `above = true` when headroom is insufficient below.
|
|
1102
|
+
*
|
|
1103
|
+
* @param sx - Source anchor x.
|
|
1104
|
+
* @param sy - Source row center y.
|
|
1105
|
+
* @param tx - Target anchor x.
|
|
1106
|
+
* @param ty - Target row center y.
|
|
1107
|
+
* @param leftEntry - Whether the link enters the target on its left edge.
|
|
1108
|
+
* @param exitRight - Whether the link exits the source on its right edge.
|
|
1109
|
+
* @param above - Route the detour above the bar row instead of below (default `false`).
|
|
1110
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1111
|
+
*/
|
|
1112
|
+
function routeSameRow(sx, sy, tx, ty, leftEntry, exitRight, above = false) {
|
|
1113
|
+
if (Math.abs(sx - tx) >= STUB_THRESHOLD) {
|
|
1114
|
+
if (leftEntry && sx < tx || !leftEntry && sx > tx) return [{
|
|
1115
|
+
x: sx,
|
|
1116
|
+
y: sy
|
|
1117
|
+
}, {
|
|
1118
|
+
x: tx,
|
|
1119
|
+
y: ty
|
|
1120
|
+
}];
|
|
935
1121
|
}
|
|
936
|
-
const
|
|
1122
|
+
const exitDir = exitRight ? TURN_MARGIN : -TURN_MARGIN;
|
|
1123
|
+
const detourY = sy + (above ? -SAME_ROW_DETOUR : SAME_ROW_DETOUR);
|
|
1124
|
+
const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
|
|
1125
|
+
return [
|
|
1126
|
+
{
|
|
1127
|
+
x: sx,
|
|
1128
|
+
y: sy
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
x: sx + exitDir,
|
|
1132
|
+
y: sy
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
x: sx + exitDir,
|
|
1136
|
+
y: detourY
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
x: approachX,
|
|
1140
|
+
y: detourY
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
x: approachX,
|
|
1144
|
+
y: ty
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
x: tx,
|
|
1148
|
+
y: ty
|
|
1149
|
+
}
|
|
1150
|
+
];
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Routes a link between **different** rows using an orthogonal path.
|
|
1154
|
+
*
|
|
1155
|
+
* 1. Step **away** from the source bar to the crossover x (`crossX`).
|
|
1156
|
+
* 2. Travel **vertically** to the midpoint between rows (`midY`).
|
|
1157
|
+
* 3. Travel **horizontally** to the approach point on the correct side
|
|
1158
|
+
* of the target.
|
|
1159
|
+
* 4. Travel **vertically** to the target row (`ty`).
|
|
1160
|
+
* 5. Final segment to the target entry point (`tx`).
|
|
1161
|
+
*
|
|
1162
|
+
* The crossover x is clamped so the path never doubles back past both bars.
|
|
1163
|
+
* When exit and entry are on the **same** side (SS / FF) the exit-side step
|
|
1164
|
+
* is limited to the approach x, avoiding a wide U‑shape.
|
|
1165
|
+
*
|
|
1166
|
+
* The approach point is chosen so the last segment travels in the arrow
|
|
1167
|
+
* direction demanded by the entry side:
|
|
1168
|
+
*
|
|
1169
|
+
* - Left‑entry (FS, SS): approach from the **left** → `tx - TURN_MARGIN`
|
|
1170
|
+
* Last segment goes RIGHT.
|
|
1171
|
+
* - Right‑entry (FF, SF): approach from the **right** → `tx + TURN_MARGIN`
|
|
1172
|
+
* Last segment goes LEFT.
|
|
1173
|
+
*
|
|
1174
|
+
* left‑entry (FS / SS) right‑entry (FF / SF)
|
|
1175
|
+
* ───────────────────── ─────────────────────
|
|
1176
|
+
* sx ●────────────────► sx ●────────────────►
|
|
1177
|
+
* exitDir exitDir
|
|
1178
|
+
* │ │
|
|
1179
|
+
* │ midY │ midY
|
|
1180
|
+
* ▼ ════════════════════► ▼ ════════════════════►
|
|
1181
|
+
* │ │
|
|
1182
|
+
* │ │
|
|
1183
|
+
* ▼ approachFromLeft ▼ approachFromRight
|
|
1184
|
+
* ●────────────────► ◄────────────────●
|
|
1185
|
+
* tx tx
|
|
1186
|
+
*
|
|
1187
|
+
* @param sx - Source anchor x.
|
|
1188
|
+
* @param sy - Source row center y.
|
|
1189
|
+
* @param tx - Target anchor x.
|
|
1190
|
+
* @param ty - Target row center y.
|
|
1191
|
+
* @param leftEntry - Whether the link enters the target on its left edge.
|
|
1192
|
+
* @param exitRight - Whether the link exits the source on its right edge.
|
|
1193
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1194
|
+
*/
|
|
1195
|
+
function routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight) {
|
|
1196
|
+
const midY = Math.round(Math.abs(sy - ty) / ROW_HEIGHT) % 2 === 0 ? (sy + ty) / 2 + ROW_HEIGHT / 2 : (sy + ty) / 2;
|
|
1197
|
+
const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
|
|
1198
|
+
const crossX = exitRight ? Math.max(sx + TURN_MARGIN, approachX) : Math.min(sx - TURN_MARGIN, approachX);
|
|
937
1199
|
return [
|
|
938
1200
|
{
|
|
939
1201
|
x: sx,
|
|
940
1202
|
y: sy
|
|
941
1203
|
},
|
|
942
1204
|
{
|
|
943
|
-
x:
|
|
1205
|
+
x: crossX,
|
|
944
1206
|
y: sy
|
|
945
1207
|
},
|
|
946
1208
|
{
|
|
947
|
-
x:
|
|
948
|
-
y:
|
|
1209
|
+
x: crossX,
|
|
1210
|
+
y: midY
|
|
949
1211
|
},
|
|
950
1212
|
{
|
|
951
|
-
x:
|
|
952
|
-
y:
|
|
1213
|
+
x: approachX,
|
|
1214
|
+
y: midY
|
|
953
1215
|
},
|
|
954
1216
|
{
|
|
955
|
-
x:
|
|
1217
|
+
x: approachX,
|
|
956
1218
|
y: ty
|
|
957
1219
|
},
|
|
958
1220
|
{
|
|
@@ -962,11 +1224,61 @@ function route(type, src, tgt) {
|
|
|
962
1224
|
];
|
|
963
1225
|
}
|
|
964
1226
|
/**
|
|
1227
|
+
* Produces the vertex list for an orthogonal connector between source and target.
|
|
1228
|
+
*
|
|
1229
|
+
* ## Anchor points (sx / tx)
|
|
1230
|
+
*
|
|
1231
|
+
* | Type | Source anchor (`sx`) | Target anchor (`tx`) |
|
|
1232
|
+
* |------|-----------------------------|-------------------------------|
|
|
1233
|
+
* | FS | right edge of source | left edge of target |
|
|
1234
|
+
* | SS | left edge of source | left edge of target |
|
|
1235
|
+
* | FF | right edge of source | right edge of target |
|
|
1236
|
+
* | SF | left edge of source | right edge of target |
|
|
1237
|
+
*
|
|
1238
|
+
* Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
|
|
1239
|
+
* ± width for zero‑width milestones.
|
|
1240
|
+
*
|
|
1241
|
+
* ## Routing strategy
|
|
1242
|
+
*
|
|
1243
|
+
* **Same row** (|sy − ty| < 1 px):
|
|
1244
|
+
* - Direct horizontal line when non‑degenerate **and** the arrow direction
|
|
1245
|
+
* naturally points **into** the target (see {@link routeSameRow}).
|
|
1246
|
+
* - Otherwise a 6‑vertex detour below the bars is drawn.
|
|
1247
|
+
*
|
|
1248
|
+
* **Different rows**: always a 6‑vertex orthogonal path that steps away from
|
|
1249
|
+
* the source, passes through the midpoint between rows, and approaches the
|
|
1250
|
+
* target from the correct side (see {@link routeMultiRow}).
|
|
1251
|
+
*
|
|
1252
|
+
* ## Arrowhead direction guarantee
|
|
1253
|
+
*
|
|
1254
|
+
* The SVG `marker-end` uses `orient="auto"`, so the arrow rotates to match
|
|
1255
|
+
* the last segment. This function ensures the last segment always travels
|
|
1256
|
+
* **into** the target on the semantically correct edge:
|
|
1257
|
+
*
|
|
1258
|
+
* | Entry side | Target edge | Last segment direction |
|
|
1259
|
+
* |-----------|-------------|-----------------------|
|
|
1260
|
+
* | left | left edge | → RIGHT |
|
|
1261
|
+
* | right | right edge | ← LEFT |
|
|
1262
|
+
*
|
|
1263
|
+
* @param type - The link type determining start/end anchor points.
|
|
1264
|
+
* @param src - The source bar layout.
|
|
1265
|
+
* @param tgt - The target bar layout.
|
|
1266
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1267
|
+
*/
|
|
1268
|
+
function route(type, src, tgt) {
|
|
1269
|
+
const { sx, tx } = getAnchors(type, src, tgt);
|
|
1270
|
+
const sy = src.centerY;
|
|
1271
|
+
const ty = tgt.centerY;
|
|
1272
|
+
const leftEntry = isLeftEntry(type);
|
|
1273
|
+
const exitRight = isExitRight(type);
|
|
1274
|
+
return collapseStubs(Math.abs(sy - ty) < 1 ? routeSameRow(sx, sy, tx, ty, leftEntry, exitRight) : routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight));
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
965
1277
|
* Computes orthogonal routing for all dependency links.
|
|
966
1278
|
* Links whose source or target is not in the layout map are skipped silently
|
|
967
1279
|
* (e.g. when the row is collapsed).
|
|
968
1280
|
*
|
|
969
|
-
* @param links
|
|
1281
|
+
* @param links - The dependency links to route.
|
|
970
1282
|
* @param layouts - A map from task ID to its computed {@link BarLayout}.
|
|
971
1283
|
* @returns An array of {@link RoutedLink} objects with computed vertex paths.
|
|
972
1284
|
*/
|
|
@@ -979,12 +1291,13 @@ function routeLinks(links, layouts) {
|
|
|
979
1291
|
linkId: link.id,
|
|
980
1292
|
sourceTaskId: link.source,
|
|
981
1293
|
targetTaskId: link.target,
|
|
1294
|
+
type: link.type,
|
|
982
1295
|
points: route(link.type, src, tgt)
|
|
983
1296
|
};
|
|
984
1297
|
}).filter((r) => r !== null);
|
|
985
1298
|
}
|
|
986
1299
|
//#endregion
|
|
987
|
-
//#region src/
|
|
1300
|
+
//#region src/lib/vanilla/dom/helpers.ts
|
|
988
1301
|
/**
|
|
989
1302
|
* Batches style assignments; avoids repeated style recalculations.
|
|
990
1303
|
*
|
|
@@ -1030,7 +1343,7 @@ function setAttrs(elem, attrs) {
|
|
|
1030
1343
|
for (const [k, v] of Object.entries(attrs)) elem.setAttribute(k, String(v));
|
|
1031
1344
|
}
|
|
1032
1345
|
//#endregion
|
|
1033
|
-
//#region src/
|
|
1346
|
+
//#region src/lib/vanilla/dom/timeHeader.ts
|
|
1034
1347
|
function specialDayKind(date, specialDaysByDate, showWeekends, weekendDays) {
|
|
1035
1348
|
const dateKey = startOfDay(date).toISOString().slice(0, 10);
|
|
1036
1349
|
const specialDay = specialDaysByDate.get(dateKey);
|
|
@@ -1168,7 +1481,7 @@ function renderTimeHeader(container, state) {
|
|
|
1168
1481
|
container.append(lowerRow);
|
|
1169
1482
|
}
|
|
1170
1483
|
//#endregion
|
|
1171
|
-
//#region src/
|
|
1484
|
+
//#region src/lib/vanilla/dom/gridColumns.ts
|
|
1172
1485
|
const DEFAULT_GRID_COLUMNS = [
|
|
1173
1486
|
{
|
|
1174
1487
|
id: "name",
|
|
@@ -1249,8 +1562,8 @@ function visibleColumns(columns) {
|
|
|
1249
1562
|
return columns.filter((c) => c.visible !== false);
|
|
1250
1563
|
}
|
|
1251
1564
|
const GRID_COLUMN_FR_MIN_WIDTH = 120;
|
|
1252
|
-
const PX_RE = /^(\d+(?:\.\d+)?)px
|
|
1253
|
-
const FR_RE = /^(\d+(?:\.\d+)?)fr
|
|
1565
|
+
const PX_RE = /^(\d+(?:\.\d+)?)px$/u;
|
|
1566
|
+
const FR_RE = /^(\d+(?:\.\d+)?)fr$/u;
|
|
1254
1567
|
function parseColumnMinWidth(width) {
|
|
1255
1568
|
const trimmed = width.trim();
|
|
1256
1569
|
const pxMatch = PX_RE.exec(trimmed);
|
|
@@ -1272,21 +1585,45 @@ function gridNaturalWidth(columns) {
|
|
|
1272
1585
|
return total;
|
|
1273
1586
|
}
|
|
1274
1587
|
//#endregion
|
|
1275
|
-
//#region src/
|
|
1588
|
+
//#region src/lib/vanilla/dom/leftPane.ts
|
|
1276
1589
|
const INDENT = 16;
|
|
1277
1590
|
const COLUMN_MIN_WIDTH = 30;
|
|
1278
|
-
function toTask$1(
|
|
1279
|
-
|
|
1280
|
-
id:
|
|
1281
|
-
text:
|
|
1282
|
-
startDate:
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
open: row.open,
|
|
1287
|
-
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1288
|
-
...row.color === void 0 ? {} : { color: row.color }
|
|
1591
|
+
function toTask$1(node) {
|
|
1592
|
+
const base = {
|
|
1593
|
+
id: node.id,
|
|
1594
|
+
text: node.text,
|
|
1595
|
+
startDate: node.startDate,
|
|
1596
|
+
...node.parent === void 0 ? {} : { parent: node.parent },
|
|
1597
|
+
...node.color === void 0 ? {} : { color: node.color },
|
|
1598
|
+
...node.data === void 0 ? {} : { data: node.data }
|
|
1289
1599
|
};
|
|
1600
|
+
switch (node.kind) {
|
|
1601
|
+
case "task": return {
|
|
1602
|
+
...base,
|
|
1603
|
+
kind: "task",
|
|
1604
|
+
durationHours: node.durationHours,
|
|
1605
|
+
percentComplete: node.percentComplete
|
|
1606
|
+
};
|
|
1607
|
+
case "project": return {
|
|
1608
|
+
...base,
|
|
1609
|
+
kind: "project",
|
|
1610
|
+
durationHours: node.durationHours,
|
|
1611
|
+
percentComplete: node.percentComplete,
|
|
1612
|
+
open: node.open
|
|
1613
|
+
};
|
|
1614
|
+
case "milestone": return {
|
|
1615
|
+
...base,
|
|
1616
|
+
kind: "milestone"
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
function getTaskField(task, field) {
|
|
1621
|
+
switch (field) {
|
|
1622
|
+
case "durationHours": return task.kind !== "milestone" ? task.durationHours : void 0;
|
|
1623
|
+
case "percentComplete": return task.kind !== "milestone" ? task.percentComplete : void 0;
|
|
1624
|
+
case "open": return task.kind === "project" ? task.open : void 0;
|
|
1625
|
+
default: return task[field];
|
|
1626
|
+
}
|
|
1290
1627
|
}
|
|
1291
1628
|
function buildTreeNameCell(row, expandedIds, cbs) {
|
|
1292
1629
|
const hasChildren = isParent(row);
|
|
@@ -1330,7 +1667,7 @@ function buildTreeNameCell(row, expandedIds, cbs) {
|
|
|
1330
1667
|
const label = el("span");
|
|
1331
1668
|
css(label, {
|
|
1332
1669
|
fontSize: "var(--gantt-font-size-md)",
|
|
1333
|
-
fontWeight: row.
|
|
1670
|
+
fontWeight: row.kind === "project" ? "var(--gantt-font-weight-bold)" : "var(--gantt-font-weight-normal)",
|
|
1334
1671
|
color: "var(--gantt-text)",
|
|
1335
1672
|
overflow: "hidden",
|
|
1336
1673
|
textOverflow: "ellipsis",
|
|
@@ -1354,9 +1691,9 @@ function buildDataCell(row, column, locale) {
|
|
|
1354
1691
|
css(cell, styles);
|
|
1355
1692
|
const task = toTask$1(row);
|
|
1356
1693
|
if (column.field !== void 0) {
|
|
1357
|
-
const rawValue = task
|
|
1694
|
+
const rawValue = getTaskField(task, column.field);
|
|
1358
1695
|
if (column.format !== void 0) cell.textContent = column.format(rawValue, task, row, locale);
|
|
1359
|
-
else cell.textContent = rawValue !== null && rawValue !== void 0 ? String(rawValue) : "";
|
|
1696
|
+
else cell.textContent = rawValue !== null && rawValue !== void 0 ? typeof rawValue === "object" ? JSON.stringify(rawValue) : String(rawValue) : "";
|
|
1360
1697
|
}
|
|
1361
1698
|
return cell;
|
|
1362
1699
|
}
|
|
@@ -1375,7 +1712,7 @@ function buildAddButton(row, cbs, locale) {
|
|
|
1375
1712
|
});
|
|
1376
1713
|
btn.addEventListener("click", (event) => {
|
|
1377
1714
|
event.stopPropagation();
|
|
1378
|
-
cbs.
|
|
1715
|
+
cbs.onTaskAdd(row.id);
|
|
1379
1716
|
});
|
|
1380
1717
|
return btn;
|
|
1381
1718
|
}
|
|
@@ -1415,7 +1752,7 @@ function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
|
|
|
1415
1752
|
wrapper.addEventListener("keydown", (event) => {
|
|
1416
1753
|
if (event.key === "Enter" || event.key === " ") {
|
|
1417
1754
|
event.preventDefault();
|
|
1418
|
-
cbs.
|
|
1755
|
+
cbs.onTaskClick(row.id);
|
|
1419
1756
|
}
|
|
1420
1757
|
});
|
|
1421
1758
|
for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
|
|
@@ -1566,23 +1903,22 @@ function setupColumnResize(headerEl, bodyEl, columns, onChange) {
|
|
|
1566
1903
|
};
|
|
1567
1904
|
}
|
|
1568
1905
|
//#endregion
|
|
1569
|
-
//#region src/
|
|
1906
|
+
//#region src/lib/vanilla/dom/dependencyLayer.ts
|
|
1570
1907
|
const NS = "http://www.w3.org/2000/svg";
|
|
1908
|
+
const ARROW_PATH = "M 0 1 L 10 5 L 0 9 Z";
|
|
1571
1909
|
const ARROW_SIZE = 6;
|
|
1572
1910
|
/**
|
|
1573
1911
|
* Creates the SVG overlay element. Call once; pass to updateDependencyLayer on each render.
|
|
1574
1912
|
* Also creates a hidden ghost-line path used during link-creation drags.
|
|
1575
1913
|
*
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1914
|
+
* The SVG is initially zero-sized; `updateDependencyLayer` sets width/height each frame.
|
|
1915
|
+
*
|
|
1916
|
+
* @param _totalWidth - The total pixel width of the SVG viewport.
|
|
1917
|
+
* @param _totalHeight - The total pixel height of the SVG viewport.
|
|
1578
1918
|
* @returns An `SVGSVGElement` ready to be inserted into the DOM.
|
|
1579
1919
|
*/
|
|
1580
|
-
function createDependencyLayer(
|
|
1920
|
+
function createDependencyLayer(_totalWidth, _totalHeight) {
|
|
1581
1921
|
const svg = document.createElementNS(NS, "svg");
|
|
1582
|
-
setAttrs(svg, {
|
|
1583
|
-
width: totalWidth,
|
|
1584
|
-
height: totalHeight
|
|
1585
|
-
});
|
|
1586
1922
|
Object.assign(svg.style, {
|
|
1587
1923
|
position: "absolute",
|
|
1588
1924
|
top: "0",
|
|
@@ -1597,7 +1933,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1597
1933
|
setAttrs(marker, {
|
|
1598
1934
|
id,
|
|
1599
1935
|
viewBox: "0 0 10 10",
|
|
1600
|
-
refX: "
|
|
1936
|
+
refX: "10",
|
|
1601
1937
|
refY: "5",
|
|
1602
1938
|
markerWidth: ARROW_SIZE,
|
|
1603
1939
|
markerHeight: ARROW_SIZE,
|
|
@@ -1605,7 +1941,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1605
1941
|
});
|
|
1606
1942
|
const path = document.createElementNS(NS, "path");
|
|
1607
1943
|
setAttrs(path, {
|
|
1608
|
-
d:
|
|
1944
|
+
d: ARROW_PATH,
|
|
1609
1945
|
fill: color
|
|
1610
1946
|
});
|
|
1611
1947
|
marker.append(path);
|
|
@@ -1638,10 +1974,9 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1638
1974
|
function showGhostLine(svg, x1, y1, x2, y2, valid) {
|
|
1639
1975
|
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
1640
1976
|
if (ghost === null) return;
|
|
1641
|
-
setAttrs(ghost, {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
});
|
|
1977
|
+
setAttrs(ghost, { d: `M ${x1},${y1} L ${x2},${y2}` });
|
|
1978
|
+
if (valid) ghost.removeAttribute("stroke-dasharray");
|
|
1979
|
+
else ghost.setAttribute("stroke-dasharray", "5 3");
|
|
1645
1980
|
if (valid) ghost.setAttribute("marker-end", "url(#gantt-arrow)");
|
|
1646
1981
|
else ghost.removeAttribute("marker-end");
|
|
1647
1982
|
ghost.style.display = "";
|
|
@@ -1668,26 +2003,21 @@ function hideGhostLine(svg) {
|
|
|
1668
2003
|
* @param totalHeight - The total pixel height of the SVG viewport.
|
|
1669
2004
|
* @param selectedTaskId - The currently selected task ID, or `null`.
|
|
1670
2005
|
* @param highlightLinkedDependenciesOnSelect - When `true`, links connected to the selected task use highlight styling.
|
|
2006
|
+
* @param cbs - Optional callbacks for link click and double-click events.
|
|
1671
2007
|
*/
|
|
1672
|
-
function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect) {
|
|
2008
|
+
function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect, cbs) {
|
|
1673
2009
|
setAttrs(svg, {
|
|
1674
2010
|
width: totalWidth,
|
|
1675
2011
|
height: totalHeight
|
|
1676
2012
|
});
|
|
1677
|
-
const toRemove = [];
|
|
1678
|
-
for (let i = 1; i < svg.children.length; i++) {
|
|
1679
|
-
const child = svg.children[i];
|
|
1680
|
-
if (child !== void 0 && !child.classList.contains("gantt-ghost-line")) toRemove.push(child);
|
|
1681
|
-
}
|
|
2013
|
+
const toRemove = [...svg.children].slice(1).filter((c) => !c.classList.contains("gantt-ghost-line"));
|
|
1682
2014
|
for (const node of toRemove) svg.removeChild(node);
|
|
2015
|
+
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
1683
2016
|
for (const link of links) {
|
|
1684
2017
|
const { points } = link;
|
|
1685
2018
|
if (points.length === 0) continue;
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const p = points[i];
|
|
1689
|
-
if (p !== void 0) d += ` L ${p.x},${p.y}`;
|
|
1690
|
-
}
|
|
2019
|
+
const [first, ...rest] = points;
|
|
2020
|
+
const d = `M ${first.x},${first.y}${rest.map((p) => ` L ${p.x},${p.y}`).join("")}`;
|
|
1691
2021
|
const isRelated = highlightLinkedDependenciesOnSelect && selectedTaskId !== null && (link.sourceTaskId === selectedTaskId || link.targetTaskId === selectedTaskId);
|
|
1692
2022
|
const path = document.createElementNS(NS, "path");
|
|
1693
2023
|
setAttrs(path, {
|
|
@@ -1696,25 +2026,62 @@ function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTask
|
|
|
1696
2026
|
stroke: isRelated ? "var(--gantt-link-hi)" : "var(--gantt-link)",
|
|
1697
2027
|
"stroke-width": isRelated ? "1.8" : "1.5",
|
|
1698
2028
|
"stroke-linejoin": "round",
|
|
1699
|
-
"marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)"
|
|
2029
|
+
"marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)",
|
|
2030
|
+
"data-link-id": String(link.linkId)
|
|
2031
|
+
});
|
|
2032
|
+
path.style.pointerEvents = "visibleStroke";
|
|
2033
|
+
path.style.cursor = "pointer";
|
|
2034
|
+
path.addEventListener("click", (event) => {
|
|
2035
|
+
if (event.detail === 1) cbs?.onLinkClick?.({
|
|
2036
|
+
id: link.linkId,
|
|
2037
|
+
source: link.sourceTaskId,
|
|
2038
|
+
target: link.targetTaskId,
|
|
2039
|
+
type: link.type
|
|
2040
|
+
});
|
|
1700
2041
|
});
|
|
1701
|
-
|
|
2042
|
+
path.addEventListener("dblclick", (_event) => {
|
|
2043
|
+
cbs?.onLinkDblClick?.({
|
|
2044
|
+
id: link.linkId,
|
|
2045
|
+
source: link.sourceTaskId,
|
|
2046
|
+
target: link.targetTaskId,
|
|
2047
|
+
type: link.type
|
|
2048
|
+
});
|
|
2049
|
+
});
|
|
2050
|
+
if (ghost !== null) svg.insertBefore(path, ghost);
|
|
2051
|
+
else svg.append(path);
|
|
1702
2052
|
}
|
|
1703
2053
|
}
|
|
1704
2054
|
//#endregion
|
|
1705
|
-
//#region src/
|
|
1706
|
-
function toTask(
|
|
1707
|
-
|
|
1708
|
-
id:
|
|
1709
|
-
text:
|
|
1710
|
-
startDate:
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1716
|
-
...row.color === void 0 ? {} : { color: row.color }
|
|
2055
|
+
//#region src/lib/vanilla/interaction/drag.ts
|
|
2056
|
+
function toTask(node) {
|
|
2057
|
+
const base = {
|
|
2058
|
+
id: node.id,
|
|
2059
|
+
text: node.text,
|
|
2060
|
+
startDate: node.startDate,
|
|
2061
|
+
...node.parent === void 0 ? {} : { parent: node.parent },
|
|
2062
|
+
...node.color === void 0 ? {} : { color: node.color },
|
|
2063
|
+
...node.readonly === void 0 ? {} : { readonly: node.readonly },
|
|
2064
|
+
...node.data === void 0 ? {} : { data: node.data }
|
|
1717
2065
|
};
|
|
2066
|
+
switch (node.kind) {
|
|
2067
|
+
case "task": return {
|
|
2068
|
+
...base,
|
|
2069
|
+
kind: "task",
|
|
2070
|
+
durationHours: node.durationHours,
|
|
2071
|
+
percentComplete: node.percentComplete
|
|
2072
|
+
};
|
|
2073
|
+
case "project": return {
|
|
2074
|
+
...base,
|
|
2075
|
+
kind: "project",
|
|
2076
|
+
durationHours: node.durationHours,
|
|
2077
|
+
percentComplete: node.percentComplete,
|
|
2078
|
+
open: node.open
|
|
2079
|
+
};
|
|
2080
|
+
case "milestone": return {
|
|
2081
|
+
...base,
|
|
2082
|
+
kind: "milestone"
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
1718
2085
|
}
|
|
1719
2086
|
/**
|
|
1720
2087
|
* Attaches drag-to-move and resize listeners to a bar element.
|
|
@@ -1736,22 +2103,27 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1736
2103
|
try {
|
|
1737
2104
|
barEl.setPointerCapture(e.pointerId);
|
|
1738
2105
|
} catch {}
|
|
1739
|
-
cbs.
|
|
2106
|
+
cbs.onTaskClick?.(task.id);
|
|
1740
2107
|
const startX = e.clientX;
|
|
1741
2108
|
const originDate = parseDate(task.startDate);
|
|
1742
2109
|
const mapper = getMapper();
|
|
2110
|
+
let lastHours = 0;
|
|
1743
2111
|
function onMove(me) {
|
|
1744
2112
|
const dx = me.clientX - startX;
|
|
1745
|
-
|
|
1746
|
-
cbs.
|
|
2113
|
+
lastHours = Math.round(mapper.widthToDuration(dx));
|
|
2114
|
+
cbs.onTaskMove?.({
|
|
1747
2115
|
id: task.id,
|
|
1748
|
-
startDate: addHours(originDate,
|
|
2116
|
+
startDate: addHours(originDate, lastHours)
|
|
1749
2117
|
});
|
|
1750
2118
|
}
|
|
1751
2119
|
function onUp() {
|
|
1752
2120
|
window.removeEventListener("pointermove", onMove);
|
|
1753
2121
|
window.removeEventListener("pointerup", onUp);
|
|
1754
2122
|
barEl.style.cursor = "grab";
|
|
2123
|
+
if (lastHours !== 0) cbs._onTaskMoveFinal?.({
|
|
2124
|
+
id: task.id,
|
|
2125
|
+
startDate: addHours(originDate, lastHours)
|
|
2126
|
+
});
|
|
1755
2127
|
}
|
|
1756
2128
|
barEl.style.cursor = "grabbing";
|
|
1757
2129
|
window.addEventListener("pointermove", onMove);
|
|
@@ -1765,29 +2137,33 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1765
2137
|
resizeHandleEl.setPointerCapture(e.pointerId);
|
|
1766
2138
|
} catch {}
|
|
1767
2139
|
const startX = e.clientX;
|
|
1768
|
-
const origDur = task.durationHours;
|
|
2140
|
+
const origDur = task.kind !== "milestone" ? task.durationHours : 0;
|
|
1769
2141
|
const mapper = getMapper();
|
|
2142
|
+
let lastDuration = origDur;
|
|
1770
2143
|
function onMove(me) {
|
|
1771
2144
|
const dx = me.clientX - startX;
|
|
1772
2145
|
const hoursDelta = Math.round(mapper.widthToDuration(dx));
|
|
1773
|
-
|
|
2146
|
+
lastDuration = Math.max(1, origDur + hoursDelta);
|
|
2147
|
+
cbs.onTaskResize?.({
|
|
1774
2148
|
id: task.id,
|
|
1775
|
-
durationHours:
|
|
2149
|
+
durationHours: lastDuration
|
|
1776
2150
|
});
|
|
1777
2151
|
}
|
|
1778
2152
|
function onUp() {
|
|
1779
2153
|
window.removeEventListener("pointermove", onMove);
|
|
1780
2154
|
window.removeEventListener("pointerup", onUp);
|
|
2155
|
+
cbs._onTaskResizeFinal?.({
|
|
2156
|
+
id: task.id,
|
|
2157
|
+
durationHours: lastDuration
|
|
2158
|
+
});
|
|
1781
2159
|
}
|
|
1782
2160
|
window.addEventListener("pointermove", onMove);
|
|
1783
2161
|
window.addEventListener("pointerup", onUp);
|
|
1784
2162
|
}
|
|
1785
2163
|
function onBarClick(event) {
|
|
1786
2164
|
if (event.detail !== 2) return;
|
|
1787
|
-
cbs.
|
|
2165
|
+
cbs.onTaskDoubleClick?.({
|
|
1788
2166
|
id: task.id,
|
|
1789
|
-
source: "bar",
|
|
1790
|
-
trigger: "doubleClick",
|
|
1791
2167
|
task: toTask(task)
|
|
1792
2168
|
});
|
|
1793
2169
|
}
|
|
@@ -1801,6 +2177,56 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1801
2177
|
};
|
|
1802
2178
|
}
|
|
1803
2179
|
/**
|
|
2180
|
+
* Attaches drag-to-change-progress listeners to a progress overlay element.
|
|
2181
|
+
*
|
|
2182
|
+
* @param progressEl - The progress overlay DOM element.
|
|
2183
|
+
* @param barEl - The bar DOM element (for width measurement).
|
|
2184
|
+
* @param task - The {@link TaskNode} for this bar.
|
|
2185
|
+
* @param _getMapper - A function returning the current {@link PixelMapper} (unused, kept for API symmetry).
|
|
2186
|
+
* @param cbs - The chart callbacks.
|
|
2187
|
+
* @returns A cleanup function that removes all listeners.
|
|
2188
|
+
*/
|
|
2189
|
+
function attachProgressDrag(progressEl, barEl, task, _getMapper, cbs) {
|
|
2190
|
+
function onProgressDown(e) {
|
|
2191
|
+
if (e.button !== 0) return;
|
|
2192
|
+
e.preventDefault();
|
|
2193
|
+
e.stopPropagation();
|
|
2194
|
+
cbs.onTaskClick?.(task.id);
|
|
2195
|
+
try {
|
|
2196
|
+
progressEl.setPointerCapture(e.pointerId);
|
|
2197
|
+
} catch {}
|
|
2198
|
+
const startX = e.clientX;
|
|
2199
|
+
const barWidth = barEl.getBoundingClientRect().width;
|
|
2200
|
+
const origPercent = task.kind !== "milestone" ? task.percentComplete ?? 0 : 0;
|
|
2201
|
+
let lastPercent = origPercent;
|
|
2202
|
+
function onMove(me) {
|
|
2203
|
+
const dx = me.clientX - startX;
|
|
2204
|
+
const percentDelta = barWidth > 0 ? dx / barWidth * 100 : 0;
|
|
2205
|
+
lastPercent = Math.max(0, Math.min(100, Math.round(origPercent + percentDelta)));
|
|
2206
|
+
cbs.onTaskProgressDrag?.({
|
|
2207
|
+
id: task.id,
|
|
2208
|
+
percentComplete: lastPercent
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
function onUp() {
|
|
2212
|
+
window.removeEventListener("pointermove", onMove);
|
|
2213
|
+
window.removeEventListener("pointerup", onUp);
|
|
2214
|
+
progressEl.style.cursor = "ew-resize";
|
|
2215
|
+
cbs._onTaskProgressDragFinal?.({
|
|
2216
|
+
id: task.id,
|
|
2217
|
+
percentComplete: lastPercent
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
progressEl.style.cursor = "ew-resize";
|
|
2221
|
+
window.addEventListener("pointermove", onMove);
|
|
2222
|
+
window.addEventListener("pointerup", onUp);
|
|
2223
|
+
}
|
|
2224
|
+
progressEl.addEventListener("pointerdown", onProgressDown);
|
|
2225
|
+
return () => {
|
|
2226
|
+
progressEl.removeEventListener("pointerdown", onProgressDown);
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
1804
2230
|
* Attaches click-to-select on a milestone diamond.
|
|
1805
2231
|
*
|
|
1806
2232
|
* @param diamondEl - The milestone diamond DOM element.
|
|
@@ -1810,16 +2236,14 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1810
2236
|
*/
|
|
1811
2237
|
function attachMilestoneClick(diamondEl, taskId, cbs) {
|
|
1812
2238
|
function onClick() {
|
|
1813
|
-
cbs.
|
|
2239
|
+
cbs.onTaskClick?.(taskId);
|
|
1814
2240
|
}
|
|
1815
2241
|
function onDoubleClick(event) {
|
|
1816
2242
|
if (event.detail === 2) {
|
|
1817
2243
|
const task = diamondEl.__task;
|
|
1818
2244
|
if (task === void 0) return;
|
|
1819
|
-
cbs.
|
|
2245
|
+
cbs.onTaskDoubleClick?.({
|
|
1820
2246
|
id: taskId,
|
|
1821
|
-
source: "milestone",
|
|
1822
|
-
trigger: "doubleClick",
|
|
1823
2247
|
task
|
|
1824
2248
|
});
|
|
1825
2249
|
}
|
|
@@ -1835,7 +2259,7 @@ function bindMilestoneTask(diamondEl, task) {
|
|
|
1835
2259
|
diamondEl.__task = task;
|
|
1836
2260
|
}
|
|
1837
2261
|
//#endregion
|
|
1838
|
-
//#region src/
|
|
2262
|
+
//#region src/lib/vanilla/interaction/linkCreation.ts
|
|
1839
2263
|
/**
|
|
1840
2264
|
* Attaches a link-creation drag listener to an endpoint handle.
|
|
1841
2265
|
*
|
|
@@ -1917,7 +2341,7 @@ function createEndpointHandle() {
|
|
|
1917
2341
|
return handle;
|
|
1918
2342
|
}
|
|
1919
2343
|
//#endregion
|
|
1920
|
-
//#region src/
|
|
2344
|
+
//#region src/lib/vanilla/dom/rightPane.ts
|
|
1921
2345
|
const BAR_COLOR = {
|
|
1922
2346
|
task: "var(--gantt-task)",
|
|
1923
2347
|
project: "var(--gantt-project)",
|
|
@@ -1942,11 +2366,16 @@ function createRightPaneRefs() {
|
|
|
1942
2366
|
scrollContainer.append(stripeContainer);
|
|
1943
2367
|
scrollContainer.append(absoluteLayer);
|
|
1944
2368
|
absoluteLayer.append(svgLayer);
|
|
2369
|
+
const tooltipEl = el("div");
|
|
2370
|
+
tooltipEl.className = "gantt-tooltip";
|
|
2371
|
+
tooltipEl.style.display = "none";
|
|
2372
|
+
scrollContainer.append(tooltipEl);
|
|
1945
2373
|
return {
|
|
1946
2374
|
scrollContainer,
|
|
1947
2375
|
stripeContainer,
|
|
1948
2376
|
absoluteLayer,
|
|
1949
2377
|
svgLayer,
|
|
2378
|
+
tooltipEl,
|
|
1950
2379
|
barRegistry: /* @__PURE__ */ new Map()
|
|
1951
2380
|
};
|
|
1952
2381
|
}
|
|
@@ -1991,9 +2420,10 @@ function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
|
|
|
1991
2420
|
cur = next;
|
|
1992
2421
|
}
|
|
1993
2422
|
}
|
|
1994
|
-
function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs) {
|
|
2423
|
+
function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs, tooltipEl) {
|
|
1995
2424
|
const selected = task.id === selectedId;
|
|
1996
|
-
const
|
|
2425
|
+
const readonly = task.readonly === true;
|
|
2426
|
+
const color = BAR_COLOR[layout.kind] ?? BAR_COLOR["task"];
|
|
1997
2427
|
const bar = el("div");
|
|
1998
2428
|
bar.className = `gantt-bar${selected ? " gantt-bar--selected gantt-shape--selected" : ""}`;
|
|
1999
2429
|
css(bar, {
|
|
@@ -2003,15 +2433,17 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2003
2433
|
width: `${layout.width}px`,
|
|
2004
2434
|
height: `${layout.height}px`,
|
|
2005
2435
|
...color === void 0 ? {} : { background: color },
|
|
2006
|
-
borderRadius: layout.
|
|
2007
|
-
cursor: "grab",
|
|
2436
|
+
borderRadius: layout.kind === "project" ? "3px" : "4px",
|
|
2437
|
+
cursor: readonly ? "pointer" : "grab",
|
|
2008
2438
|
userSelect: "none",
|
|
2009
2439
|
overflow: "hidden",
|
|
2010
2440
|
zIndex: selected ? "3" : "2",
|
|
2011
2441
|
touchAction: "none"
|
|
2012
2442
|
});
|
|
2443
|
+
let cleanupProgressDrag;
|
|
2013
2444
|
if (layout.progressWidth > 0) {
|
|
2014
2445
|
const prog = el("div");
|
|
2446
|
+
const progressEnabled = state.progressDragEnabled;
|
|
2015
2447
|
css(prog, {
|
|
2016
2448
|
position: "absolute",
|
|
2017
2449
|
left: "0",
|
|
@@ -2019,8 +2451,15 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2019
2451
|
width: `${layout.progressWidth}px`,
|
|
2020
2452
|
height: "100%",
|
|
2021
2453
|
background: "rgba(0,0,0,0.18)",
|
|
2022
|
-
|
|
2454
|
+
...progressEnabled ? {
|
|
2455
|
+
cursor: "ew-resize",
|
|
2456
|
+
touchAction: "none"
|
|
2457
|
+
} : { pointerEvents: "none" }
|
|
2023
2458
|
});
|
|
2459
|
+
if (progressEnabled) {
|
|
2460
|
+
prog.className = "gantt-progress-overlay";
|
|
2461
|
+
cleanupProgressDrag = attachProgressDrag(prog, bar, task, () => state.mapper, cbs);
|
|
2462
|
+
}
|
|
2024
2463
|
bar.append(prog);
|
|
2025
2464
|
}
|
|
2026
2465
|
const label = el("span");
|
|
@@ -2046,30 +2485,38 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2046
2485
|
bar.setAttribute("aria-label", ariaLabel(state.locale, "ariaTask", task.text));
|
|
2047
2486
|
bar.setAttribute("aria-pressed", String(selected));
|
|
2048
2487
|
bar.dataset["taskId"] = String(task.id);
|
|
2049
|
-
bar.addEventListener("click", () => {
|
|
2050
|
-
cbs.
|
|
2488
|
+
bar.addEventListener("click", (event) => {
|
|
2489
|
+
if (event.detail === 2) cbs.onTaskDoubleClick?.({
|
|
2490
|
+
id: task.id,
|
|
2491
|
+
task: toTask(task)
|
|
2492
|
+
});
|
|
2493
|
+
else cbs.onTaskClick?.(task.id);
|
|
2051
2494
|
});
|
|
2052
2495
|
bar.addEventListener("keydown", (event) => {
|
|
2053
2496
|
if (event.key === "Enter" || event.key === " ") {
|
|
2054
2497
|
event.preventDefault();
|
|
2055
|
-
cbs.
|
|
2498
|
+
cbs.onTaskClick?.(task.id);
|
|
2056
2499
|
}
|
|
2057
2500
|
});
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2501
|
+
let handle;
|
|
2502
|
+
let cleanupDrag;
|
|
2503
|
+
if (!readonly) {
|
|
2504
|
+
handle = el("div");
|
|
2505
|
+
handle.className = "gantt-resize-handle";
|
|
2506
|
+
css(handle, {
|
|
2507
|
+
position: "absolute",
|
|
2508
|
+
right: "0",
|
|
2509
|
+
top: "0",
|
|
2510
|
+
width: "8px",
|
|
2511
|
+
height: "100%",
|
|
2512
|
+
cursor: "ew-resize",
|
|
2513
|
+
zIndex: "1",
|
|
2514
|
+
touchAction: "none"
|
|
2515
|
+
});
|
|
2516
|
+
bar.append(handle);
|
|
2517
|
+
layer.insertBefore(bar, svgLayer);
|
|
2518
|
+
cleanupDrag = attachDrag(bar, handle, task, () => state.mapper, cbs);
|
|
2519
|
+
} else layer.insertBefore(bar, svgLayer);
|
|
2073
2520
|
let cleanupLinkHandles;
|
|
2074
2521
|
if (state.linkCreationEnabled) {
|
|
2075
2522
|
const barCenterY = layout.y + layout.height / 2;
|
|
@@ -2104,16 +2551,52 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2104
2551
|
bar.removeEventListener("mouseleave", onBarLeave);
|
|
2105
2552
|
};
|
|
2106
2553
|
}
|
|
2554
|
+
const onTooltipEnter = () => {
|
|
2555
|
+
const content = cbs.onTooltipText?.({
|
|
2556
|
+
id: task.id,
|
|
2557
|
+
task: toTask(task)
|
|
2558
|
+
});
|
|
2559
|
+
if (content && content.length > 0) {
|
|
2560
|
+
tooltipEl.innerHTML = content;
|
|
2561
|
+
tooltipEl.style.display = "";
|
|
2562
|
+
} else tooltipEl.style.display = "none";
|
|
2563
|
+
};
|
|
2564
|
+
const onTooltipMove = (e) => {
|
|
2565
|
+
const offsetX = 12;
|
|
2566
|
+
const offsetY = -8;
|
|
2567
|
+
let left = e.clientX + offsetX;
|
|
2568
|
+
let top = e.clientY + offsetY;
|
|
2569
|
+
const maxLeft = window.innerWidth - tooltipEl.offsetWidth - 4;
|
|
2570
|
+
const maxTop = window.innerHeight - tooltipEl.offsetHeight - 4;
|
|
2571
|
+
left = Math.max(4, Math.min(left, maxLeft));
|
|
2572
|
+
top = Math.max(4, Math.min(top, maxTop));
|
|
2573
|
+
tooltipEl.style.left = `${left}px`;
|
|
2574
|
+
tooltipEl.style.top = `${top}px`;
|
|
2575
|
+
};
|
|
2576
|
+
const onTooltipLeave = () => {
|
|
2577
|
+
tooltipEl.style.display = "none";
|
|
2578
|
+
};
|
|
2579
|
+
bar.addEventListener("mouseenter", onTooltipEnter);
|
|
2580
|
+
bar.addEventListener("mousemove", onTooltipMove);
|
|
2581
|
+
bar.addEventListener("mouseleave", onTooltipLeave);
|
|
2582
|
+
const cleanupTooltip = () => {
|
|
2583
|
+
bar.removeEventListener("mouseenter", onTooltipEnter);
|
|
2584
|
+
bar.removeEventListener("mousemove", onTooltipMove);
|
|
2585
|
+
bar.removeEventListener("mouseleave", onTooltipLeave);
|
|
2586
|
+
};
|
|
2107
2587
|
const entry = {
|
|
2108
2588
|
bar,
|
|
2109
|
-
resizeHandle: handle
|
|
2110
|
-
cleanupDrag
|
|
2589
|
+
resizeHandle: handle ?? el("div")
|
|
2111
2590
|
};
|
|
2591
|
+
if (cleanupDrag !== void 0) entry.cleanupDrag = cleanupDrag;
|
|
2112
2592
|
if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
|
|
2593
|
+
if (cleanupProgressDrag !== void 0) entry.cleanupProgressDrag = cleanupProgressDrag;
|
|
2594
|
+
if (cleanupTooltip !== void 0) entry.cleanupTooltip = cleanupTooltip;
|
|
2113
2595
|
registry.set(task.id, entry);
|
|
2114
2596
|
}
|
|
2115
|
-
function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state) {
|
|
2597
|
+
function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state, tooltipEl) {
|
|
2116
2598
|
const selected = task.id === selectedId;
|
|
2599
|
+
const readonly = task.readonly === true;
|
|
2117
2600
|
const size = MILESTONE_HALF * 2;
|
|
2118
2601
|
const diamond = el("div");
|
|
2119
2602
|
diamond.className = `gantt-milestone${selected ? " gantt-shape--selected" : ""}`;
|
|
@@ -2125,7 +2608,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
2125
2608
|
height: `${size}px`,
|
|
2126
2609
|
background: "var(--gantt-milestone)",
|
|
2127
2610
|
transform: "rotate(45deg)",
|
|
2128
|
-
cursor: "pointer",
|
|
2611
|
+
cursor: readonly ? "default" : "pointer",
|
|
2129
2612
|
zIndex: "4"
|
|
2130
2613
|
});
|
|
2131
2614
|
diamond.tabIndex = 0;
|
|
@@ -2136,7 +2619,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
2136
2619
|
diamond.addEventListener("keydown", (event) => {
|
|
2137
2620
|
if (event.key === "Enter" || event.key === " ") {
|
|
2138
2621
|
event.preventDefault();
|
|
2139
|
-
cbs.
|
|
2622
|
+
cbs.onTaskClick?.(task.id);
|
|
2140
2623
|
}
|
|
2141
2624
|
});
|
|
2142
2625
|
const labelEl = el("span");
|
|
@@ -2156,7 +2639,15 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
2156
2639
|
layer.insertBefore(diamond, svgLayer);
|
|
2157
2640
|
bindMilestoneTask(diamond, task);
|
|
2158
2641
|
const dummy = el("div");
|
|
2159
|
-
|
|
2642
|
+
let cleanupDrag;
|
|
2643
|
+
if (!readonly) cleanupDrag = attachMilestoneClick(diamond, task.id, cbs);
|
|
2644
|
+
else diamond.addEventListener("click", (event) => {
|
|
2645
|
+
if (event.detail === 2) cbs.onTaskDoubleClick?.({
|
|
2646
|
+
id: task.id,
|
|
2647
|
+
task: toTask(task)
|
|
2648
|
+
});
|
|
2649
|
+
else cbs.onTaskClick?.(task.id);
|
|
2650
|
+
});
|
|
2160
2651
|
let cleanupLinkHandles;
|
|
2161
2652
|
if (state.linkCreationEnabled) {
|
|
2162
2653
|
const diamondCenterY = layout.y + layout.height / 2;
|
|
@@ -2182,12 +2673,46 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
2182
2673
|
diamond.removeEventListener("mouseleave", onDiamondLeave);
|
|
2183
2674
|
};
|
|
2184
2675
|
}
|
|
2676
|
+
const onTooltipEnter = () => {
|
|
2677
|
+
const content = cbs.onTooltipText?.({
|
|
2678
|
+
id: task.id,
|
|
2679
|
+
task: toTask(task)
|
|
2680
|
+
});
|
|
2681
|
+
if (content && content.length > 0) {
|
|
2682
|
+
tooltipEl.innerHTML = content;
|
|
2683
|
+
tooltipEl.style.display = "";
|
|
2684
|
+
} else tooltipEl.style.display = "none";
|
|
2685
|
+
};
|
|
2686
|
+
const onTooltipMove = (e) => {
|
|
2687
|
+
const offsetX = 12;
|
|
2688
|
+
const offsetY = -8;
|
|
2689
|
+
let left = e.clientX + offsetX;
|
|
2690
|
+
let top = e.clientY + offsetY;
|
|
2691
|
+
const maxLeft = window.innerWidth - tooltipEl.offsetWidth - 4;
|
|
2692
|
+
const maxTop = window.innerHeight - tooltipEl.offsetHeight - 4;
|
|
2693
|
+
left = Math.max(4, Math.min(left, maxLeft));
|
|
2694
|
+
top = Math.max(4, Math.min(top, maxTop));
|
|
2695
|
+
tooltipEl.style.left = `${left}px`;
|
|
2696
|
+
tooltipEl.style.top = `${top}px`;
|
|
2697
|
+
};
|
|
2698
|
+
const onTooltipLeave = () => {
|
|
2699
|
+
tooltipEl.style.display = "none";
|
|
2700
|
+
};
|
|
2701
|
+
diamond.addEventListener("mouseenter", onTooltipEnter);
|
|
2702
|
+
diamond.addEventListener("mousemove", onTooltipMove);
|
|
2703
|
+
diamond.addEventListener("mouseleave", onTooltipLeave);
|
|
2704
|
+
const cleanupTooltip = () => {
|
|
2705
|
+
diamond.removeEventListener("mouseenter", onTooltipEnter);
|
|
2706
|
+
diamond.removeEventListener("mousemove", onTooltipMove);
|
|
2707
|
+
diamond.removeEventListener("mouseleave", onTooltipLeave);
|
|
2708
|
+
};
|
|
2185
2709
|
const entry = {
|
|
2186
2710
|
bar: diamond,
|
|
2187
|
-
resizeHandle: dummy
|
|
2188
|
-
cleanupDrag
|
|
2711
|
+
resizeHandle: dummy
|
|
2189
2712
|
};
|
|
2713
|
+
if (cleanupDrag !== void 0) entry.cleanupDrag = cleanupDrag;
|
|
2190
2714
|
if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
|
|
2715
|
+
if (cleanupTooltip !== void 0) entry.cleanupTooltip = cleanupTooltip;
|
|
2191
2716
|
registry.set(task.id, entry);
|
|
2192
2717
|
}
|
|
2193
2718
|
/**
|
|
@@ -2235,9 +2760,10 @@ function renderRightPane(refs, state, cbs) {
|
|
|
2235
2760
|
for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
|
|
2236
2761
|
for (const node of toRemove) absoluteLayer.removeChild(node);
|
|
2237
2762
|
hideGhostLine(svgLayer);
|
|
2238
|
-
for (const { cleanupDrag, cleanupLinkHandles } of barRegistry.values()) {
|
|
2239
|
-
cleanupDrag();
|
|
2763
|
+
for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of barRegistry.values()) {
|
|
2764
|
+
cleanupDrag?.();
|
|
2240
2765
|
cleanupLinkHandles?.();
|
|
2766
|
+
cleanupProgressDrag?.();
|
|
2241
2767
|
}
|
|
2242
2768
|
barRegistry.clear();
|
|
2243
2769
|
if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
|
|
@@ -2278,13 +2804,13 @@ function renderRightPane(refs, state, cbs) {
|
|
|
2278
2804
|
for (const task of visibleRows) {
|
|
2279
2805
|
const layout = layouts.get(task.id);
|
|
2280
2806
|
if (layout === void 0) continue;
|
|
2281
|
-
if (layout.
|
|
2282
|
-
else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
|
|
2807
|
+
if (layout.kind === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state, refs.tooltipEl);
|
|
2808
|
+
else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs, refs.tooltipEl);
|
|
2283
2809
|
}
|
|
2284
|
-
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect);
|
|
2810
|
+
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect, cbs);
|
|
2285
2811
|
}
|
|
2286
2812
|
//#endregion
|
|
2287
|
-
//#region src/
|
|
2813
|
+
//#region src/lib/vanilla/utils.ts
|
|
2288
2814
|
function buildTaskIndex(tasks) {
|
|
2289
2815
|
const index = /* @__PURE__ */ new Map();
|
|
2290
2816
|
for (let i = 0; i < tasks.length; i++) {
|
|
@@ -2333,11 +2859,11 @@ function getExpandableTaskIds(tasks) {
|
|
|
2333
2859
|
function getInitialExpandedIds(tasks) {
|
|
2334
2860
|
const expandableIds = getExpandableTaskIds(tasks);
|
|
2335
2861
|
const expandedIds = /* @__PURE__ */ new Set();
|
|
2336
|
-
for (const task of tasks) if (task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
|
|
2862
|
+
for (const task of tasks) if (task.kind === "project" && task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
|
|
2337
2863
|
return expandedIds;
|
|
2338
2864
|
}
|
|
2339
2865
|
//#endregion
|
|
2340
|
-
//#region src/
|
|
2866
|
+
//#region src/lib/vanilla/splitter.ts
|
|
2341
2867
|
const MIN_PANE_WIDTH$1 = 96;
|
|
2342
2868
|
function attachSplitter(splitterHandle, leftPane, container, timelineMinWidth, onDragEnd) {
|
|
2343
2869
|
splitterHandle.addEventListener("pointerdown", (e) => {
|
|
@@ -2386,7 +2912,7 @@ function computeLeftPaneWidth(options) {
|
|
|
2386
2912
|
return Math.max(MIN_PANE_WIDTH, Math.floor(width));
|
|
2387
2913
|
}
|
|
2388
2914
|
//#endregion
|
|
2389
|
-
//#region src/
|
|
2915
|
+
//#region src/lib/vanilla/gantt-chart.ts
|
|
2390
2916
|
const HEADER_H = 52;
|
|
2391
2917
|
const OVERSCAN = 4;
|
|
2392
2918
|
/**
|
|
@@ -2405,6 +2931,7 @@ const OVERSCAN = 4;
|
|
|
2405
2931
|
var GanttChart = class {
|
|
2406
2932
|
#container;
|
|
2407
2933
|
#opts;
|
|
2934
|
+
#callbacks;
|
|
2408
2935
|
#input = null;
|
|
2409
2936
|
#scale;
|
|
2410
2937
|
#selectedId = null;
|
|
@@ -2412,6 +2939,7 @@ var GanttChart = class {
|
|
|
2412
2939
|
#rafPending = false;
|
|
2413
2940
|
#rafId = null;
|
|
2414
2941
|
#destroyed = false;
|
|
2942
|
+
#dragOriginals = /* @__PURE__ */ new Map();
|
|
2415
2943
|
#taskIndex;
|
|
2416
2944
|
#lastGridClick = null;
|
|
2417
2945
|
#userSplitWidth = null;
|
|
@@ -2426,6 +2954,7 @@ var GanttChart = class {
|
|
|
2426
2954
|
#root;
|
|
2427
2955
|
#scrollEl;
|
|
2428
2956
|
#leftPane;
|
|
2957
|
+
#leftHeader;
|
|
2429
2958
|
#leftBody;
|
|
2430
2959
|
#rightPane;
|
|
2431
2960
|
#rightHeader;
|
|
@@ -2436,14 +2965,16 @@ var GanttChart = class {
|
|
|
2436
2965
|
/**
|
|
2437
2966
|
* Constructs a new chart, builds the DOM, and wires internal event handling.
|
|
2438
2967
|
* Data must be loaded via {@link update} before the chart renders.
|
|
2968
|
+
* Callbacks must be set via {@link setCallbacks} before user interactions are handled.
|
|
2439
2969
|
*
|
|
2440
2970
|
* @param container - The host `HTMLElement` the chart will be appended to.
|
|
2441
|
-
* @param opts - Configuration
|
|
2971
|
+
* @param opts - Configuration options.
|
|
2442
2972
|
*/
|
|
2443
2973
|
constructor(container, opts = {}) {
|
|
2444
2974
|
this.#container = container;
|
|
2445
2975
|
this.#scale = opts.scale ?? "day";
|
|
2446
2976
|
this.#opts = opts;
|
|
2977
|
+
this.#callbacks = {};
|
|
2447
2978
|
this.#taskIndex = /* @__PURE__ */ new Map();
|
|
2448
2979
|
this.#locale = resolveChartLocale(opts.locale);
|
|
2449
2980
|
this.#columns = opts.gridColumns ?? gridColumnDefaults(this.#locale);
|
|
@@ -2453,50 +2984,189 @@ var GanttChart = class {
|
|
|
2453
2984
|
this.#weekendDays = normalizeWeekendDays(opts.weekendDays ?? this.#locale.weekendDays);
|
|
2454
2985
|
this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays ?? []);
|
|
2455
2986
|
this.#expandedIds = /* @__PURE__ */ new Set();
|
|
2456
|
-
this.#cbs =
|
|
2457
|
-
|
|
2987
|
+
this.#cbs = this.#buildCallbackAdapter();
|
|
2988
|
+
this.#buildDom();
|
|
2989
|
+
this.#wireEvents();
|
|
2990
|
+
container.append(this.#root);
|
|
2991
|
+
this.#applyTheme();
|
|
2992
|
+
this.#applyResponsivePaneStyles();
|
|
2993
|
+
this.#setupResizeObserver();
|
|
2994
|
+
}
|
|
2995
|
+
#buildCallbackAdapter() {
|
|
2996
|
+
return {
|
|
2997
|
+
onTaskClick: (id) => {
|
|
2458
2998
|
if (this.#selectedId === id) return;
|
|
2459
2999
|
this.#selectedId = id;
|
|
2460
|
-
|
|
3000
|
+
if (this.#selectedId !== null) {
|
|
3001
|
+
const task = this.#findTask(this.#selectedId);
|
|
3002
|
+
if (task !== void 0) this.#callbacks.onTaskClick?.({
|
|
3003
|
+
task,
|
|
3004
|
+
instance: this
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
2461
3007
|
this.#scheduleRender();
|
|
2462
3008
|
},
|
|
2463
3009
|
onTaskDoubleClick: (payload) => {
|
|
2464
|
-
|
|
3010
|
+
this.#callbacks.onTaskDoubleClick?.({
|
|
3011
|
+
task: payload.task,
|
|
3012
|
+
instance: this
|
|
3013
|
+
});
|
|
2465
3014
|
},
|
|
2466
3015
|
onTaskEditIntent: (payload) => {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
source: payload.source
|
|
3016
|
+
this.#callbacks.onTaskDoubleClick?.({
|
|
3017
|
+
task: payload.task,
|
|
3018
|
+
instance: this
|
|
2471
3019
|
});
|
|
2472
3020
|
},
|
|
2473
|
-
|
|
3021
|
+
onTaskMove: (payload) => {
|
|
3022
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
3023
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
3024
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
3025
|
+
}
|
|
2474
3026
|
const iso = payload.startDate.toISOString().slice(0, 10);
|
|
2475
3027
|
this.#patchTask(payload.id, { startDate: iso });
|
|
2476
|
-
opts.onMove?.(payload);
|
|
2477
3028
|
this.#scheduleRender();
|
|
2478
3029
|
},
|
|
2479
|
-
|
|
3030
|
+
_onTaskMoveFinal: async (payload) => {
|
|
3031
|
+
const task = this.#findTask(payload.id);
|
|
3032
|
+
if (task !== void 0) {
|
|
3033
|
+
const result = this.#callbacks.onTaskMove?.({
|
|
3034
|
+
task,
|
|
3035
|
+
newStartDate: payload.startDate,
|
|
3036
|
+
instance: this
|
|
3037
|
+
});
|
|
3038
|
+
if (result instanceof Promise) {
|
|
3039
|
+
if (!await result) {
|
|
3040
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3041
|
+
if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
|
|
3042
|
+
}
|
|
3043
|
+
} else if (!result) {
|
|
3044
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3045
|
+
if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
this.#dragOriginals.clear();
|
|
3049
|
+
this.#scheduleRender();
|
|
3050
|
+
return true;
|
|
3051
|
+
},
|
|
3052
|
+
onTaskResize: (payload) => {
|
|
3053
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
3054
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
3055
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
3056
|
+
}
|
|
2480
3057
|
this.#patchTask(payload.id, { durationHours: payload.durationHours });
|
|
2481
|
-
opts.onResize?.(payload);
|
|
2482
3058
|
this.#scheduleRender();
|
|
2483
3059
|
},
|
|
3060
|
+
_onTaskResizeFinal: async (payload) => {
|
|
3061
|
+
const task = this.#findTask(payload.id);
|
|
3062
|
+
if (task !== void 0) {
|
|
3063
|
+
const result = this.#callbacks.onTaskResize?.({
|
|
3064
|
+
task,
|
|
3065
|
+
newDurationHours: payload.durationHours,
|
|
3066
|
+
instance: this
|
|
3067
|
+
});
|
|
3068
|
+
if (result instanceof Promise) {
|
|
3069
|
+
if (!await result) {
|
|
3070
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3071
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
|
|
3072
|
+
}
|
|
3073
|
+
} else if (!result) {
|
|
3074
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3075
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
this.#dragOriginals.clear();
|
|
3079
|
+
this.#scheduleRender();
|
|
3080
|
+
return true;
|
|
3081
|
+
},
|
|
3082
|
+
onTaskProgressDrag: (payload) => {
|
|
3083
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
3084
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
3085
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
3086
|
+
}
|
|
3087
|
+
this.#patchTask(payload.id, { percentComplete: payload.percentComplete });
|
|
3088
|
+
this.#scheduleRender();
|
|
3089
|
+
},
|
|
3090
|
+
_onTaskProgressDragFinal: async (payload) => {
|
|
3091
|
+
const task = this.#findTask(payload.id);
|
|
3092
|
+
if (task !== void 0) {
|
|
3093
|
+
const result = this.#callbacks.onProgressChange?.({
|
|
3094
|
+
task,
|
|
3095
|
+
newPercentComplete: payload.percentComplete,
|
|
3096
|
+
instance: this
|
|
3097
|
+
});
|
|
3098
|
+
if (result instanceof Promise) {
|
|
3099
|
+
if (!await result) {
|
|
3100
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3101
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
|
|
3102
|
+
}
|
|
3103
|
+
} else if (!result) {
|
|
3104
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
3105
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
this.#dragOriginals.clear();
|
|
3109
|
+
this.#scheduleRender();
|
|
3110
|
+
return true;
|
|
3111
|
+
},
|
|
3112
|
+
onTaskAdd: (parentId) => {
|
|
3113
|
+
const parentTask = this.#findTask(parentId);
|
|
3114
|
+
if (parentTask !== void 0) this.#callbacks.onTaskAdd?.({
|
|
3115
|
+
parentTask,
|
|
3116
|
+
instance: this
|
|
3117
|
+
});
|
|
3118
|
+
},
|
|
2484
3119
|
onLeftPaneWidthChange: (width) => {
|
|
2485
|
-
|
|
3120
|
+
this.#callbacks.onLeftPaneWidthChange?.({
|
|
3121
|
+
width,
|
|
3122
|
+
instance: this
|
|
3123
|
+
});
|
|
2486
3124
|
},
|
|
2487
3125
|
onGridColumnsChange: (updatedColumns) => {
|
|
2488
|
-
|
|
3126
|
+
this.#callbacks.onGridColumnsChange?.({
|
|
3127
|
+
columns: updatedColumns,
|
|
3128
|
+
instance: this
|
|
3129
|
+
});
|
|
2489
3130
|
},
|
|
2490
3131
|
onLinkCreate: (payload) => {
|
|
2491
|
-
|
|
2492
|
-
|
|
3132
|
+
const sourceTask = this.#findTask(payload.sourceTaskId);
|
|
3133
|
+
const targetTask = this.#findTask(payload.targetTaskId);
|
|
3134
|
+
if (sourceTask !== void 0 && targetTask !== void 0) this.#callbacks.onLinkCreate?.({
|
|
3135
|
+
type: "FS",
|
|
3136
|
+
sourceTask,
|
|
3137
|
+
targetTask,
|
|
3138
|
+
instance: this
|
|
3139
|
+
});
|
|
3140
|
+
},
|
|
3141
|
+
onLinkClick: (payload) => {
|
|
3142
|
+
this.#callbacks.onLinkClick?.({
|
|
3143
|
+
link: payload,
|
|
3144
|
+
instance: this
|
|
3145
|
+
});
|
|
3146
|
+
},
|
|
3147
|
+
onLinkDblClick: (payload) => {
|
|
3148
|
+
this.#callbacks.onLinkDblClick?.({
|
|
3149
|
+
link: payload,
|
|
3150
|
+
instance: this
|
|
3151
|
+
});
|
|
3152
|
+
},
|
|
3153
|
+
onTooltipText: (payload) => this.#callbacks.onTooltipText?.({
|
|
3154
|
+
task: payload.task,
|
|
3155
|
+
instance: this
|
|
3156
|
+
}) ?? null
|
|
2493
3157
|
};
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Sets or replaces the chart's user-facing callbacks.
|
|
3161
|
+
* Does not trigger a re-render.
|
|
3162
|
+
*
|
|
3163
|
+
* @param cbs - The {@link GanttCallbacks} to register.
|
|
3164
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
3165
|
+
*/
|
|
3166
|
+
setCallbacks(cbs) {
|
|
3167
|
+
this.#assertAlive();
|
|
3168
|
+
this.#callbacks = cbs;
|
|
3169
|
+
this.#cbs = this.#buildCallbackAdapter();
|
|
2500
3170
|
}
|
|
2501
3171
|
/**
|
|
2502
3172
|
* Replaces the full dataset and re-renders.
|
|
@@ -2508,9 +3178,9 @@ var GanttChart = class {
|
|
|
2508
3178
|
this.#assertAlive();
|
|
2509
3179
|
validateLinkRefs(newInput.tasks, newInput.links);
|
|
2510
3180
|
detectCycles(newInput.tasks, newInput.links);
|
|
2511
|
-
this.#input = newInput;
|
|
2512
|
-
this.#taskIndex = buildTaskIndex(
|
|
2513
|
-
this.#expandedIds = getInitialExpandedIds(
|
|
3181
|
+
this.#input = structuredClone(newInput);
|
|
3182
|
+
this.#taskIndex = buildTaskIndex(this.#input.tasks);
|
|
3183
|
+
this.#expandedIds = getInitialExpandedIds(this.#input.tasks);
|
|
2514
3184
|
if (this.#rafPending && this.#rafId !== null) {
|
|
2515
3185
|
cancelAnimationFrame(this.#rafId);
|
|
2516
3186
|
this.#rafId = null;
|
|
@@ -2519,26 +3189,78 @@ var GanttChart = class {
|
|
|
2519
3189
|
this.#render();
|
|
2520
3190
|
}
|
|
2521
3191
|
/**
|
|
2522
|
-
*
|
|
3192
|
+
* Merges the supplied options into the current configuration and re-renders
|
|
3193
|
+
* only the panes affected by the changed options.
|
|
2523
3194
|
*
|
|
2524
|
-
* @param
|
|
3195
|
+
* @param opts - A partial {@link GanttOptions} object. Only the keys present
|
|
3196
|
+
* in this parameter are updated; missing keys keep their
|
|
3197
|
+
* previous values.
|
|
2525
3198
|
* @throws {GanttError} When the instance has been destroyed.
|
|
2526
3199
|
*/
|
|
2527
|
-
|
|
3200
|
+
setOptions(opts) {
|
|
2528
3201
|
this.#assertAlive();
|
|
2529
|
-
this.#
|
|
2530
|
-
this.#
|
|
3202
|
+
Object.assign(this.#opts, opts);
|
|
3203
|
+
this.#scale = this.#opts.scale ?? "day";
|
|
3204
|
+
let columnsChanged = false;
|
|
3205
|
+
if (opts.locale !== void 0) {
|
|
3206
|
+
this.#locale = resolveChartLocale(opts.locale);
|
|
3207
|
+
if (this.#opts.gridColumns === void 0) {
|
|
3208
|
+
this.#columns = gridColumnDefaults(this.#locale);
|
|
3209
|
+
this.#leftPaneDefaultWidth = gridNaturalWidth(this.#columns);
|
|
3210
|
+
columnsChanged = true;
|
|
3211
|
+
}
|
|
3212
|
+
if (this.#opts.weekendDays === void 0) this.#weekendDays = normalizeWeekendDays(this.#locale.weekendDays);
|
|
3213
|
+
}
|
|
3214
|
+
if (opts.gridColumns !== void 0) {
|
|
3215
|
+
this.#columns = opts.gridColumns;
|
|
3216
|
+
this.#leftPaneDefaultWidth = this.#opts.leftPaneWidth ?? gridNaturalWidth(this.#columns);
|
|
3217
|
+
columnsChanged = true;
|
|
3218
|
+
}
|
|
3219
|
+
if (columnsChanged && this.#input !== null) this.#rebuildLeftPaneHeader();
|
|
3220
|
+
if (opts.leftPaneWidth !== void 0) this.#leftPaneDefaultWidth = opts.leftPaneWidth;
|
|
3221
|
+
if (opts.height !== void 0) {
|
|
3222
|
+
this.#height = opts.height;
|
|
3223
|
+
this.#root.style.height = `${this.#height}px`;
|
|
3224
|
+
}
|
|
3225
|
+
if (opts.timelineMinWidth !== void 0) {
|
|
3226
|
+
this.#timelineMinWidth = opts.timelineMinWidth;
|
|
3227
|
+
this.#rightPane.style.minWidth = `${this.#timelineMinWidth}px`;
|
|
3228
|
+
}
|
|
3229
|
+
if (opts.weekendDays !== void 0) this.#weekendDays = normalizeWeekendDays(opts.weekendDays);
|
|
3230
|
+
if (opts.specialDays !== void 0) this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays);
|
|
3231
|
+
if (opts.theme !== void 0) this.#applyTheme();
|
|
3232
|
+
const hasLayoutChange = opts.leftPaneWidth !== void 0 || opts.responsiveSplitPane !== void 0 || opts.mobileBreakpoint !== void 0 || opts.mobileLeftPaneMinWidth !== void 0 || opts.mobileLeftPaneMaxRatio !== void 0 || opts.timelineMinWidth !== void 0;
|
|
3233
|
+
if (hasLayoutChange) this.#applyResponsivePaneStyles();
|
|
3234
|
+
const hasLeftPaneChange = columnsChanged || opts.locale !== void 0;
|
|
3235
|
+
const hasRightPaneChange = opts.scale !== void 0 || opts.showWeekends !== void 0 || opts.weekendDays !== void 0 || opts.specialDays !== void 0 || opts.highlightLinkedDependenciesOnSelect !== void 0 || opts.linkCreationEnabled !== void 0 || opts.progressDragEnabled !== void 0 || opts.viewportStart !== void 0 || opts.viewportEnd !== void 0 || opts.locale !== void 0 || opts.timelineMinWidth !== void 0;
|
|
3236
|
+
if (!(hasLeftPaneChange || hasRightPaneChange || hasLayoutChange)) return;
|
|
3237
|
+
if (this.#rafPending && this.#rafId !== null) {
|
|
3238
|
+
cancelAnimationFrame(this.#rafId);
|
|
3239
|
+
this.#rafId = null;
|
|
3240
|
+
this.#rafPending = false;
|
|
3241
|
+
}
|
|
3242
|
+
if (hasLeftPaneChange && !hasRightPaneChange) this.#renderGrid();
|
|
3243
|
+
else if (!hasLeftPaneChange && hasRightPaneChange) this.#renderTimeline();
|
|
3244
|
+
else this.#render();
|
|
2531
3245
|
}
|
|
2532
3246
|
/**
|
|
2533
3247
|
* Programmatically selects or deselects a task.
|
|
2534
3248
|
*
|
|
2535
3249
|
* @param id - The task ID to select, or `null` to clear the selection.
|
|
3250
|
+
* @param fireCallback - Whether to fire the `onTaskClick` callback. Default `true`.
|
|
2536
3251
|
* @throws {GanttError} When the instance has been destroyed.
|
|
2537
3252
|
*/
|
|
2538
|
-
select(id) {
|
|
3253
|
+
select(id, fireCallback = true) {
|
|
2539
3254
|
this.#assertAlive();
|
|
2540
|
-
this.#selectedId =
|
|
2541
|
-
|
|
3255
|
+
if (id === null) this.#selectedId = null;
|
|
3256
|
+
else {
|
|
3257
|
+
const task = this.#input?.tasks.find((t) => t.id === id);
|
|
3258
|
+
if (task !== void 0 && fireCallback) this.#callbacks.onTaskClick?.({
|
|
3259
|
+
task,
|
|
3260
|
+
instance: this
|
|
3261
|
+
});
|
|
3262
|
+
this.#selectedId = id;
|
|
3263
|
+
}
|
|
2542
3264
|
if (this.#rafPending && this.#rafId !== null) {
|
|
2543
3265
|
cancelAnimationFrame(this.#rafId);
|
|
2544
3266
|
this.#rafId = null;
|
|
@@ -2589,9 +3311,11 @@ var GanttChart = class {
|
|
|
2589
3311
|
else window.removeEventListener("resize", this.#applyResponsivePaneStyles);
|
|
2590
3312
|
if (this.#rafId !== null) cancelAnimationFrame(this.#rafId);
|
|
2591
3313
|
this.#columnResizeCleanup();
|
|
2592
|
-
for (const { cleanupDrag, cleanupLinkHandles } of this.#rightPaneRefs.barRegistry.values()) {
|
|
2593
|
-
cleanupDrag();
|
|
3314
|
+
for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag, cleanupTooltip } of this.#rightPaneRefs.barRegistry.values()) {
|
|
3315
|
+
cleanupDrag?.();
|
|
2594
3316
|
cleanupLinkHandles?.();
|
|
3317
|
+
cleanupProgressDrag?.();
|
|
3318
|
+
cleanupTooltip?.();
|
|
2595
3319
|
}
|
|
2596
3320
|
clearChildren(this.#container);
|
|
2597
3321
|
}
|
|
@@ -2606,15 +3330,16 @@ var GanttChart = class {
|
|
|
2606
3330
|
...patch
|
|
2607
3331
|
};
|
|
2608
3332
|
}
|
|
3333
|
+
#findTask(id) {
|
|
3334
|
+
return this.#input?.tasks.find((t) => t.id === id);
|
|
3335
|
+
}
|
|
2609
3336
|
#handleGridClick = (payload) => {
|
|
2610
3337
|
const now = Date.now();
|
|
2611
3338
|
const prev = this.#lastGridClick;
|
|
2612
3339
|
if (prev !== null && prev.id === payload.id && now - prev.atMs <= 350) {
|
|
2613
3340
|
this.#lastGridClick = null;
|
|
2614
|
-
this.#cbs.
|
|
3341
|
+
this.#cbs.onTaskDoubleClick?.({
|
|
2615
3342
|
id: payload.id,
|
|
2616
|
-
source: "grid",
|
|
2617
|
-
trigger: "doubleClick",
|
|
2618
3343
|
task: payload.task
|
|
2619
3344
|
});
|
|
2620
3345
|
return;
|
|
@@ -2623,7 +3348,7 @@ var GanttChart = class {
|
|
|
2623
3348
|
id: payload.id,
|
|
2624
3349
|
atMs: now
|
|
2625
3350
|
};
|
|
2626
|
-
this.#cbs.
|
|
3351
|
+
this.#cbs.onTaskClick?.(payload.id);
|
|
2627
3352
|
};
|
|
2628
3353
|
#onScroll = () => {
|
|
2629
3354
|
({scrollTop: this.#scrollTop} = this.#scrollEl);
|
|
@@ -2664,6 +3389,7 @@ var GanttChart = class {
|
|
|
2664
3389
|
scale: this.#scale,
|
|
2665
3390
|
highlightLinkedDependenciesOnSelect: this.#opts.highlightLinkedDependenciesOnSelect ?? false,
|
|
2666
3391
|
linkCreationEnabled: this.#opts.linkCreationEnabled ?? false,
|
|
3392
|
+
progressDragEnabled: this.#opts.progressDragEnabled ?? false,
|
|
2667
3393
|
expandedIds: this.#expandedIds,
|
|
2668
3394
|
selectedId: this.#selectedId,
|
|
2669
3395
|
scrollTop: this.#scrollTop,
|
|
@@ -2690,21 +3416,47 @@ var GanttChart = class {
|
|
|
2690
3416
|
if (input === null) return;
|
|
2691
3417
|
const state = this.#computeState(input);
|
|
2692
3418
|
renderTimeHeader(this.#rightHeader, state);
|
|
3419
|
+
this.#renderGridInternal(state);
|
|
3420
|
+
renderRightPane(this.#rightPaneRefs, state, this.#cbs);
|
|
3421
|
+
};
|
|
3422
|
+
#renderGrid = () => {
|
|
3423
|
+
this.#rafPending = false;
|
|
3424
|
+
const input = this.#input;
|
|
3425
|
+
if (input === null) return;
|
|
3426
|
+
this.#renderGridInternal(this.#computeState(input));
|
|
3427
|
+
};
|
|
3428
|
+
#renderGridInternal(state) {
|
|
2693
3429
|
renderLeftPane(this.#leftBody, state, {
|
|
2694
3430
|
onToggle: (id) => {
|
|
2695
3431
|
if (this.#expandedIds.has(id)) this.#expandedIds.delete(id);
|
|
2696
3432
|
else this.#expandedIds.add(id);
|
|
2697
3433
|
this.#scheduleRender();
|
|
2698
3434
|
},
|
|
2699
|
-
|
|
3435
|
+
onTaskClick: (id) => this.#cbs.onTaskClick?.(id),
|
|
2700
3436
|
onRowClick: (payload) => {
|
|
2701
3437
|
this.#handleGridClick(payload);
|
|
2702
3438
|
},
|
|
2703
|
-
|
|
2704
|
-
|
|
3439
|
+
onTaskDoubleClick: (payload) => this.#cbs.onTaskDoubleClick?.(payload),
|
|
3440
|
+
onTaskAdd: (id) => this.#cbs.onTaskAdd?.(id)
|
|
2705
3441
|
}, this.#columns);
|
|
3442
|
+
}
|
|
3443
|
+
#renderTimeline = () => {
|
|
3444
|
+
this.#rafPending = false;
|
|
3445
|
+
const input = this.#input;
|
|
3446
|
+
if (input === null) return;
|
|
3447
|
+
const state = this.#computeState(input);
|
|
3448
|
+
renderTimeHeader(this.#rightHeader, state);
|
|
2706
3449
|
renderRightPane(this.#rightPaneRefs, state, this.#cbs);
|
|
2707
3450
|
};
|
|
3451
|
+
#rebuildLeftPaneHeader() {
|
|
3452
|
+
this.#columnResizeCleanup();
|
|
3453
|
+
clearChildren(this.#leftHeader);
|
|
3454
|
+
const headerEl = buildLeftPaneHeader(this.#columns);
|
|
3455
|
+
this.#leftHeader.append(headerEl);
|
|
3456
|
+
this.#columnResizeCleanup = setupColumnResize(headerEl, this.#leftBody, this.#columns, (updated) => {
|
|
3457
|
+
this.#cbs.onGridColumnsChange?.(updated);
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
2708
3460
|
#scheduleRender() {
|
|
2709
3461
|
if (this.#rafPending || this.#destroyed) return;
|
|
2710
3462
|
this.#rafPending = true;
|
|
@@ -2760,6 +3512,7 @@ var GanttChart = class {
|
|
|
2760
3512
|
const headerEl = buildLeftPaneHeader(this.#columns);
|
|
2761
3513
|
leftHeader.append(headerEl);
|
|
2762
3514
|
leftPane.append(leftHeader);
|
|
3515
|
+
this.#leftHeader = leftHeader;
|
|
2763
3516
|
const leftBody = el("div");
|
|
2764
3517
|
leftPane.append(leftBody);
|
|
2765
3518
|
this.#leftBody = leftBody;
|
|
@@ -2807,12 +3560,14 @@ var GanttChart = class {
|
|
|
2807
3560
|
#wireEvents() {
|
|
2808
3561
|
this.#rightPaneRefs.absoluteLayer.addEventListener("click", (event) => {
|
|
2809
3562
|
if (event.target.closest(".gantt-bar, .gantt-milestone, .gantt-resize-handle")) return;
|
|
2810
|
-
this.#
|
|
3563
|
+
this.#selectedId = null;
|
|
3564
|
+
this.#scheduleRender();
|
|
2811
3565
|
});
|
|
2812
3566
|
this.#root.addEventListener("keydown", (event) => {
|
|
2813
3567
|
if (event.key === "Escape" && this.#selectedId !== null) {
|
|
2814
3568
|
event.preventDefault();
|
|
2815
|
-
this.#
|
|
3569
|
+
this.#selectedId = null;
|
|
3570
|
+
this.#scheduleRender();
|
|
2816
3571
|
}
|
|
2817
3572
|
});
|
|
2818
3573
|
this.#scrollEl.addEventListener("scroll", this.#onScroll);
|
|
@@ -2827,6 +3582,6 @@ var GanttChart = class {
|
|
|
2827
3582
|
}
|
|
2828
3583
|
};
|
|
2829
3584
|
//#endregion
|
|
2830
|
-
export { BAR_HEIGHT, BAR_Y_OFFSET, CHART_LOCALE_EN_US, DEFAULT_GRID_COLUMNS, DENSITY, EN_US_LABELS, GRID_COLUMN_FR_MIN_WIDTH, GanttChart, GanttError, GanttInputSchema, LinkSchema, LinkTypeSchema, MILESTONE_HALF, MILESTONE_SIZE, ROW_HEIGHT, SCALE_CONFIGS, SpecialDayKindSchema, SpecialDaySchema,
|
|
3585
|
+
export { BAR_HEIGHT, BAR_Y_OFFSET, CHART_LOCALE_EN_US, DEFAULT_GRID_COLUMNS, DENSITY, EN_US_LABELS, GRID_COLUMN_FR_MIN_WIDTH, GanttChart, GanttError, GanttInputSchema, LinkSchema, LinkTypeSchema, MILESTONE_HALF, MILESTONE_SIZE, ROW_HEIGHT, SCALE_CONFIGS, SpecialDayKindSchema, SpecialDaySchema, TaskKindSchema, TaskSchema, addDays, addHours, buildTaskTree, computeLayout, createPixelMapper, deriveViewport, deriveWeekNumbering, deriveWeekStartsOn, deriveWeekendDays, detectCycles, diffDays, diffHours, flattenTree, formatLabel, formatWeekNumber, gridColumnDefaults, gridNaturalWidth, gridTemplateColumns, isParent, parseDate, parseGanttInput, resolveChartLocale, routeLinks, validateLinkRefs, visibleColumns };
|
|
2831
3586
|
|
|
2832
3587
|
//# sourceMappingURL=index.mjs.map
|