gantt-renderer 0.2.0 → 0.3.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 +14 -0
- package/README.md +1 -1
- package/dist/index.d.mts +130 -78
- package/dist/index.mjs +832 -244
- package/dist/index.mjs.map +1 -1
- 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,60 @@ 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
|
+
/** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
|
|
34
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
35
|
+
};
|
|
36
|
+
/** @internal */
|
|
37
|
+
const TaskLeafSchema = z.object({
|
|
38
|
+
...taskBase,
|
|
39
|
+
kind: z.literal("task"),
|
|
40
|
+
/** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
|
|
41
|
+
durationHours: z.number().int().positive(),
|
|
42
|
+
/** 0–100 completion percentage (integer). */
|
|
43
|
+
percentComplete: z.number().int().min(0).max(100).default(0)
|
|
44
|
+
});
|
|
45
|
+
/** @internal */
|
|
46
|
+
const TaskProjectSchema = z.object({
|
|
47
|
+
...taskBase,
|
|
48
|
+
kind: z.literal("project"),
|
|
49
|
+
/** Duration in hours. Must be positive; use `kind: 'milestone'` for zero-duration points. */
|
|
50
|
+
durationHours: z.number().int().positive(),
|
|
51
|
+
/** 0–100 completion percentage (integer). */
|
|
34
52
|
percentComplete: z.number().int().min(0).max(100).default(0),
|
|
35
53
|
/**
|
|
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
54
|
* Initial expanded state for tree hierarchy.
|
|
47
55
|
* When `false`, children of this task are hidden on initial render.
|
|
48
|
-
* Only relevant for tasks with child tasks.
|
|
49
56
|
*
|
|
50
57
|
* @default true
|
|
51
58
|
*/
|
|
52
|
-
open: z.boolean().default(true)
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
open: z.boolean().default(true)
|
|
60
|
+
});
|
|
61
|
+
/** @internal */
|
|
62
|
+
const TaskMilestoneSchema = z.object({
|
|
63
|
+
...taskBase,
|
|
64
|
+
kind: z.literal("milestone")
|
|
55
65
|
});
|
|
66
|
+
const TaskSchema = z.discriminatedUnion("kind", [
|
|
67
|
+
TaskLeafSchema,
|
|
68
|
+
TaskProjectSchema,
|
|
69
|
+
TaskMilestoneSchema
|
|
70
|
+
]);
|
|
56
71
|
const LinkSchema = z.object({
|
|
57
72
|
/** Unique positive integer identifier for the dependency link. */
|
|
58
73
|
id: z.number().int().positive(),
|
|
@@ -70,13 +85,64 @@ const LinkSchema = z.object({
|
|
|
70
85
|
*
|
|
71
86
|
* @default 'FS'
|
|
72
87
|
*/
|
|
73
|
-
type: LinkTypeSchema.default("FS")
|
|
88
|
+
type: LinkTypeSchema.default("FS"),
|
|
89
|
+
/** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
|
|
90
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
91
|
+
}).refine((l) => l.source !== l.target, {
|
|
92
|
+
message: "A link cannot connect a task to itself",
|
|
93
|
+
path: ["target"]
|
|
74
94
|
});
|
|
75
95
|
const GanttInputSchema = z.object({
|
|
76
96
|
/** Array of task objects. At least one task is required. */
|
|
77
97
|
tasks: z.array(TaskSchema).min(1),
|
|
78
98
|
/** Optional array of dependency link objects. Defaults to empty array. */
|
|
79
99
|
links: z.array(LinkSchema).default([])
|
|
100
|
+
}).superRefine((data, ctx) => {
|
|
101
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
102
|
+
for (let i = 0; i < data.tasks.length; i++) {
|
|
103
|
+
const task = data.tasks[i];
|
|
104
|
+
if (task !== void 0) {
|
|
105
|
+
if (taskIds.has(task.id)) ctx.addIssue({
|
|
106
|
+
code: "custom",
|
|
107
|
+
message: `Duplicate task id: ${task.id}`,
|
|
108
|
+
path: [
|
|
109
|
+
"tasks",
|
|
110
|
+
i,
|
|
111
|
+
"id"
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
taskIds.add(task.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const linkIds = /* @__PURE__ */ new Set();
|
|
118
|
+
for (let i = 0; i < data.links.length; i++) {
|
|
119
|
+
const link = data.links[i];
|
|
120
|
+
if (link !== void 0) {
|
|
121
|
+
if (linkIds.has(link.id)) ctx.addIssue({
|
|
122
|
+
code: "custom",
|
|
123
|
+
message: `Duplicate link id: ${link.id}`,
|
|
124
|
+
path: [
|
|
125
|
+
"links",
|
|
126
|
+
i,
|
|
127
|
+
"id"
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
linkIds.add(link.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const pairKeys = /* @__PURE__ */ new Set();
|
|
134
|
+
for (let i = 0; i < data.links.length; i++) {
|
|
135
|
+
const link = data.links[i];
|
|
136
|
+
if (link !== void 0) {
|
|
137
|
+
const key = `${link.source}:${link.target}`;
|
|
138
|
+
if (pairKeys.has(key)) ctx.addIssue({
|
|
139
|
+
code: "custom",
|
|
140
|
+
message: `Duplicate link pair: source=${link.source} target=${link.target}`,
|
|
141
|
+
path: ["links", i]
|
|
142
|
+
});
|
|
143
|
+
pairKeys.add(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
80
146
|
});
|
|
81
147
|
/**
|
|
82
148
|
* Parses raw external data.
|
|
@@ -88,18 +154,8 @@ const GanttInputSchema = z.object({
|
|
|
88
154
|
function parseGanttInput(raw) {
|
|
89
155
|
return GanttInputSchema.parse(raw);
|
|
90
156
|
}
|
|
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
157
|
//#endregion
|
|
102
|
-
//#region src/
|
|
158
|
+
//#region src/lib/errors.ts
|
|
103
159
|
/**
|
|
104
160
|
* Domain-specific error with a machine-readable {@link GanttErrorCode}.
|
|
105
161
|
*/
|
|
@@ -116,23 +172,67 @@ var GanttError = class extends Error {
|
|
|
116
172
|
}
|
|
117
173
|
};
|
|
118
174
|
//#endregion
|
|
119
|
-
//#region src/
|
|
175
|
+
//#region src/lib/domain/tree.ts
|
|
176
|
+
function detectParentCycles(tasks) {
|
|
177
|
+
const adj = /* @__PURE__ */ new Map();
|
|
178
|
+
for (const task of tasks) {
|
|
179
|
+
adj.set(task.id, []);
|
|
180
|
+
if (task.parent !== void 0) {
|
|
181
|
+
const parents = adj.get(task.parent);
|
|
182
|
+
if (parents !== void 0) parents.push(task.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
186
|
+
const color = /* @__PURE__ */ new Map();
|
|
187
|
+
const parent = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const id of adj.keys()) color.set(id, WHITE);
|
|
189
|
+
const dfs = (u) => {
|
|
190
|
+
color.set(u, GRAY);
|
|
191
|
+
for (const v of adj.get(u) ?? []) {
|
|
192
|
+
const vc = color.get(v) ?? WHITE;
|
|
193
|
+
if (vc === GRAY) {
|
|
194
|
+
const path = [v, u];
|
|
195
|
+
let cur = u;
|
|
196
|
+
while (cur !== v) {
|
|
197
|
+
const p = parent.get(cur);
|
|
198
|
+
if (p === void 0) break;
|
|
199
|
+
path.push(p);
|
|
200
|
+
cur = p;
|
|
201
|
+
}
|
|
202
|
+
throw new GanttError("PARENT_CYCLE", `Parent cycle detected: ${[...path].reverse().join(" -> ")}`);
|
|
203
|
+
}
|
|
204
|
+
if (vc === WHITE) {
|
|
205
|
+
parent.set(v, u);
|
|
206
|
+
dfs(v);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
color.set(u, BLACK);
|
|
210
|
+
};
|
|
211
|
+
for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
|
|
212
|
+
}
|
|
120
213
|
/**
|
|
121
214
|
* Builds a typed tree from a flat task array.
|
|
122
215
|
* Order of tasks[] is irrelevant — parents need not precede children.
|
|
123
216
|
*
|
|
124
217
|
* @param tasks - The flat array of tasks to convert into a tree.
|
|
125
218
|
* @returns Root-level {@link TaskNode} instances with populated `children`.
|
|
126
|
-
* @throws {GanttError} When a task references a `parent` id that does not exist
|
|
219
|
+
* @throws {GanttError} When a task references a `parent` id that does not exist,
|
|
220
|
+
* when a parent cycle is detected, or when a `parent` points to a
|
|
221
|
+
* milestone or leaf task.
|
|
127
222
|
*/
|
|
128
223
|
function buildTaskTree(tasks) {
|
|
129
224
|
const map = /* @__PURE__ */ new Map();
|
|
130
225
|
const roots = [];
|
|
226
|
+
for (const task of tasks) if (task.parent !== void 0) {
|
|
227
|
+
const parentTask = tasks.find((t) => t.id === task.parent);
|
|
228
|
+
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}'`);
|
|
229
|
+
}
|
|
131
230
|
for (const task of tasks) map.set(task.id, {
|
|
132
231
|
...task,
|
|
133
232
|
children: [],
|
|
134
233
|
depth: 0
|
|
135
234
|
});
|
|
235
|
+
detectParentCycles(tasks);
|
|
136
236
|
for (const task of tasks) {
|
|
137
237
|
const node = map.get(task.id);
|
|
138
238
|
if (node === void 0) continue;
|
|
@@ -177,7 +277,7 @@ function isParent(node) {
|
|
|
177
277
|
return node.children.length > 0;
|
|
178
278
|
}
|
|
179
279
|
//#endregion
|
|
180
|
-
//#region src/
|
|
280
|
+
//#region src/lib/domain/dependencies.ts
|
|
181
281
|
/**
|
|
182
282
|
* Detects circular dependencies in the link graph using DFS tri-colour marking.
|
|
183
283
|
*
|
|
@@ -221,21 +321,34 @@ function detectCycles(tasks, links) {
|
|
|
221
321
|
for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
|
|
222
322
|
}
|
|
223
323
|
/**
|
|
224
|
-
* Validates that every link references existing task IDs
|
|
324
|
+
* Validates that every link references existing task IDs and that no
|
|
325
|
+
* duplicate (source, target) pairs exist.
|
|
225
326
|
*
|
|
226
327
|
* @param tasks - The task list (used as the reference set of valid IDs).
|
|
227
328
|
* @param links - The dependency links to validate.
|
|
228
|
-
* @throws {GanttError} When any link references a non-existent source or target task
|
|
329
|
+
* @throws {GanttError} When any link references a non-existent source or target task,
|
|
330
|
+
* when a non-FS link connects to/from a milestone, or when duplicate
|
|
331
|
+
* (source, target) pairs exist.
|
|
229
332
|
*/
|
|
230
333
|
function validateLinkRefs(tasks, links) {
|
|
231
334
|
const ids = new Set(tasks.map((t) => t.id));
|
|
335
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
336
|
+
const pairKeys = /* @__PURE__ */ new Set();
|
|
232
337
|
for (const link of links) {
|
|
233
338
|
if (!ids.has(link.source)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: source=${link.source} not found`);
|
|
234
339
|
if (!ids.has(link.target)) throw new GanttError("LINK_REFERENCE", `Link id=${link.id}: target=${link.target} not found`);
|
|
340
|
+
const pairKey = `${link.source}:${link.target}`;
|
|
341
|
+
if (pairKeys.has(pairKey)) throw new GanttError("DUPLICATE_LINK_PAIR", `Link id=${link.id}: duplicate pair source=${link.source} target=${link.target}`);
|
|
342
|
+
pairKeys.add(pairKey);
|
|
343
|
+
if (link.type !== "FS") {
|
|
344
|
+
const sourceTask = taskById.get(link.source);
|
|
345
|
+
const targetTask = taskById.get(link.target);
|
|
346
|
+
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`);
|
|
347
|
+
}
|
|
235
348
|
}
|
|
236
349
|
}
|
|
237
350
|
//#endregion
|
|
238
|
-
//#region src/
|
|
351
|
+
//#region src/lib/locale.ts
|
|
239
352
|
const WEEK_START_REGION = {
|
|
240
353
|
US: 0,
|
|
241
354
|
CA: 0,
|
|
@@ -517,7 +630,7 @@ function formatLabel(template, arg) {
|
|
|
517
630
|
return template.replaceAll("{0}", arg);
|
|
518
631
|
}
|
|
519
632
|
//#endregion
|
|
520
|
-
//#region src/
|
|
633
|
+
//#region src/lib/domain/dateMath.ts
|
|
521
634
|
/**
|
|
522
635
|
* Parses `YYYY-MM-DD` → UTC midnight `Date`.
|
|
523
636
|
*
|
|
@@ -657,7 +770,7 @@ function formatDisplayDate(dateStr, locale) {
|
|
|
657
770
|
});
|
|
658
771
|
}
|
|
659
772
|
//#endregion
|
|
660
|
-
//#region src/
|
|
773
|
+
//#region src/lib/timeline/scale.ts
|
|
661
774
|
const H = 36e5;
|
|
662
775
|
const D = 864e5;
|
|
663
776
|
const SCALE_CONFIGS = {
|
|
@@ -740,7 +853,7 @@ function nextScaleBoundary(date, scale) {
|
|
|
740
853
|
}
|
|
741
854
|
}
|
|
742
855
|
//#endregion
|
|
743
|
-
//#region src/
|
|
856
|
+
//#region src/lib/timeline/pixelMapper.ts
|
|
744
857
|
/**
|
|
745
858
|
* Creates a stateless pixel mapper for the given scale and viewport start.
|
|
746
859
|
* All conversions are O(1) arithmetic — safe to call in tight loops.
|
|
@@ -773,7 +886,7 @@ function createPixelMapper(scale, viewportStart) {
|
|
|
773
886
|
};
|
|
774
887
|
}
|
|
775
888
|
//#endregion
|
|
776
|
-
//#region src/
|
|
889
|
+
//#region src/lib/timeline/layoutEngine.ts
|
|
777
890
|
const DENSITY = {
|
|
778
891
|
rowHeight: 44,
|
|
779
892
|
barHeight: 28,
|
|
@@ -802,8 +915,7 @@ function computeLayout(rows, mapper) {
|
|
|
802
915
|
const x = mapper.toX(start);
|
|
803
916
|
const y = i * ROW_HEIGHT + BAR_Y_OFFSET;
|
|
804
917
|
const centerY = i * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
805
|
-
|
|
806
|
-
if (type === "milestone") {
|
|
918
|
+
if (task.kind === "milestone") {
|
|
807
919
|
result.set(task.id, {
|
|
808
920
|
taskId: task.id,
|
|
809
921
|
x,
|
|
@@ -811,7 +923,7 @@ function computeLayout(rows, mapper) {
|
|
|
811
923
|
width: 0,
|
|
812
924
|
height: BAR_HEIGHT,
|
|
813
925
|
progressWidth: 0,
|
|
814
|
-
|
|
926
|
+
kind: "milestone",
|
|
815
927
|
rowIndex: i,
|
|
816
928
|
centerX: x,
|
|
817
929
|
centerY
|
|
@@ -827,7 +939,7 @@ function computeLayout(rows, mapper) {
|
|
|
827
939
|
width,
|
|
828
940
|
height: BAR_HEIGHT,
|
|
829
941
|
progressWidth,
|
|
830
|
-
|
|
942
|
+
kind: task.kind,
|
|
831
943
|
rowIndex: i,
|
|
832
944
|
centerX: x + width / 2,
|
|
833
945
|
centerY
|
|
@@ -860,99 +972,171 @@ function deriveViewport(tasks, paddingHours = 48) {
|
|
|
860
972
|
let maxMs = -Infinity;
|
|
861
973
|
for (const task of tasks) {
|
|
862
974
|
const start = parseDate(task.startDate);
|
|
863
|
-
const end = addHours(start, task.durationHours);
|
|
864
975
|
if (start.getTime() < minMs) minMs = start.getTime();
|
|
865
|
-
if (
|
|
976
|
+
if (task.kind !== "milestone") {
|
|
977
|
+
const end = addHours(start, task.durationHours);
|
|
978
|
+
if (end.getTime() > maxMs) maxMs = end.getTime();
|
|
979
|
+
} else if (start.getTime() > maxMs) maxMs = start.getTime();
|
|
866
980
|
}
|
|
867
981
|
return [addHours(new Date(minMs), -paddingHours), addHours(new Date(maxMs), paddingHours)];
|
|
868
982
|
}
|
|
869
983
|
//#endregion
|
|
870
|
-
//#region src/
|
|
871
|
-
|
|
984
|
+
//#region src/lib/rendering/linkRouter.ts
|
|
985
|
+
/** px gap before/after bar for routing clearance and arrow approach */
|
|
986
|
+
const TURN_MARGIN = 24;
|
|
987
|
+
/** px vertical offset below the bar row for same-row loop detours */
|
|
988
|
+
const SAME_ROW_DETOUR = 24;
|
|
989
|
+
/** Segments shorter than this (px) are collapsed before stroking */
|
|
990
|
+
const STUB_THRESHOLD = 2;
|
|
872
991
|
/**
|
|
873
|
-
*
|
|
992
|
+
* Removes consecutive points whose Euclidean distance is below {@link STUB_THRESHOLD}.
|
|
993
|
+
* The first point is always kept. This prevents near‑zero‑length segments from
|
|
994
|
+
* appearing as visible stubs near the arrowhead.
|
|
874
995
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
996
|
+
* @param points - The ordered vertex list.
|
|
997
|
+
* @returns A filtered copy, guaranteed to contain at least the first point.
|
|
998
|
+
*/
|
|
999
|
+
function collapseStubs(points) {
|
|
1000
|
+
const out = [];
|
|
1001
|
+
for (const pt of points) {
|
|
1002
|
+
const last = out.at(-1);
|
|
1003
|
+
if (last === void 0 || Math.hypot(pt.x - last.x, pt.y - last.y) >= STUB_THRESHOLD) out.push(pt);
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* True when the dependency arrow enters the target on its **left** edge (FS, SS).
|
|
1009
|
+
* False when the arrow enters on the target's **right** edge (FF, SF).
|
|
1010
|
+
*
|
|
1011
|
+
* The arrowhead uses SVG `orient="auto"` so it rotates to match the direction
|
|
1012
|
+
* of the **last** path segment. Therefore:
|
|
1013
|
+
*
|
|
1014
|
+
* - Left-entry → last segment must travel **RIGHT** (penultimate.x < tx).
|
|
1015
|
+
* - Right-entry → last segment must travel **LEFT** (penultimate.x > tx).
|
|
1016
|
+
*
|
|
1017
|
+
* @param type - The link type.
|
|
1018
|
+
* @returns `true` for FS / SS, `false` for FF / SF.
|
|
1019
|
+
*/
|
|
1020
|
+
function isLeftEntry(type) {
|
|
1021
|
+
return type === "FS" || type === "SS";
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* True when the link exits the source bar on its **right** edge (FS, FF).
|
|
1025
|
+
* False when it exits on the **left** edge (SS, SF).
|
|
1026
|
+
*
|
|
1027
|
+
* The first step after the source anchor should move **away** from the bar,
|
|
1028
|
+
* **not** into it. This means:
|
|
1029
|
+
*
|
|
1030
|
+
* - Exit‑right → first horizontal segment goes RIGHT (+TURN_MARGIN).
|
|
1031
|
+
* - Exit‑left → first horizontal segment goes LEFT (-TURN_MARGIN).
|
|
1032
|
+
*
|
|
1033
|
+
* @param type - The link type.
|
|
1034
|
+
* @returns `true` for FS / FF, `false` for SS / SF.
|
|
1035
|
+
*/
|
|
1036
|
+
function isExitRight(type) {
|
|
1037
|
+
return type === "FS" || type === "FF";
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Computes anchor points for the given link type.
|
|
1041
|
+
*
|
|
1042
|
+
* | Type | Source anchor (`sx`) | Target anchor (`tx`) |
|
|
1043
|
+
* |------|-----------------------------|-------------------------------|
|
|
1044
|
+
* | FS | right edge of source | left edge of target |
|
|
1045
|
+
* | SS | left edge of source | left edge of target |
|
|
1046
|
+
* | FF | right edge of source | right edge of target |
|
|
1047
|
+
* | SF | left edge of source | right edge of target |
|
|
1048
|
+
*
|
|
1049
|
+
* Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
|
|
1050
|
+
* ± width for zero‑width milestones.
|
|
880
1051
|
*
|
|
881
1052
|
* @param type - The link type determining start/end anchor points.
|
|
882
|
-
* @param src
|
|
883
|
-
* @param tgt
|
|
884
|
-
* @returns
|
|
1053
|
+
* @param src - The source bar layout.
|
|
1054
|
+
* @param tgt - The target bar layout.
|
|
1055
|
+
* @returns Anchor x coordinates `{sx, tx}`.
|
|
1056
|
+
* @throws {Error} if the link type is not handled (exhaustiveness guard).
|
|
885
1057
|
*/
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
const
|
|
1058
|
+
function getAnchors(type, src, tgt) {
|
|
1059
|
+
const srcRight = src.kind === "milestone" ? src.x + MILESTONE_HALF : src.x + src.width;
|
|
1060
|
+
const srcLeft = src.kind === "milestone" ? src.x - MILESTONE_HALF : src.x;
|
|
1061
|
+
const tgtRight = tgt.kind === "milestone" ? tgt.x + MILESTONE_HALF : tgt.x + tgt.width;
|
|
1062
|
+
const tgtLeft = tgt.kind === "milestone" ? tgt.x - MILESTONE_HALF : tgt.x;
|
|
890
1063
|
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
|
-
|
|
1064
|
+
case "FS": return {
|
|
1065
|
+
sx: srcRight,
|
|
1066
|
+
tx: tgtLeft
|
|
1067
|
+
};
|
|
1068
|
+
case "SS": return {
|
|
1069
|
+
sx: srcLeft,
|
|
1070
|
+
tx: tgtLeft
|
|
1071
|
+
};
|
|
1072
|
+
case "FF": return {
|
|
1073
|
+
sx: srcRight,
|
|
1074
|
+
tx: tgtRight
|
|
1075
|
+
};
|
|
1076
|
+
case "SF": return {
|
|
1077
|
+
sx: srcLeft,
|
|
1078
|
+
tx: tgtRight
|
|
1079
|
+
};
|
|
1080
|
+
default: throw new Error(`Unhandled link type: ${String(type)}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Routes a link whose source and target rows are within 1 px of each other.
|
|
1085
|
+
*
|
|
1086
|
+
* **Direct‑line optimisation**
|
|
1087
|
+
* A plain horizontal segment is only used when it is non‑degenerate (`sx ≠ tx`)
|
|
1088
|
+
* AND the arrowhead direction is visually correct:
|
|
1089
|
+
*
|
|
1090
|
+
* | Entry side | Condition | Arrow direction |
|
|
1091
|
+
* |-----------|-----------|----------------|
|
|
1092
|
+
* | left | `sx < tx` | → RIGHT ✓ |
|
|
1093
|
+
* | right | `sx > tx` | ← LEFT ✓ |
|
|
1094
|
+
*
|
|
1095
|
+
* Otherwise a 6‑vertex detour is drawn so that the last segment approaches
|
|
1096
|
+
* the target from the correct side. By default the detour goes **below** the
|
|
1097
|
+
* bars; pass `above = true` when headroom is insufficient below.
|
|
1098
|
+
*
|
|
1099
|
+
* @param sx - Source anchor x.
|
|
1100
|
+
* @param sy - Source row center y.
|
|
1101
|
+
* @param tx - Target anchor x.
|
|
1102
|
+
* @param ty - Target row center y.
|
|
1103
|
+
* @param leftEntry - Whether the link enters the target on its left edge.
|
|
1104
|
+
* @param exitRight - Whether the link exits the source on its right edge.
|
|
1105
|
+
* @param above - Route the detour above the bar row instead of below (default `false`).
|
|
1106
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1107
|
+
*/
|
|
1108
|
+
function routeSameRow(sx, sy, tx, ty, leftEntry, exitRight, above = false) {
|
|
1109
|
+
if (Math.abs(sx - tx) >= STUB_THRESHOLD) {
|
|
1110
|
+
if (leftEntry && sx < tx || !leftEntry && sx > tx) return [{
|
|
1111
|
+
x: sx,
|
|
1112
|
+
y: sy
|
|
1113
|
+
}, {
|
|
1114
|
+
x: tx,
|
|
1115
|
+
y: ty
|
|
1116
|
+
}];
|
|
935
1117
|
}
|
|
936
|
-
const
|
|
1118
|
+
const exitDir = exitRight ? TURN_MARGIN : -TURN_MARGIN;
|
|
1119
|
+
const detourY = sy + (above ? -SAME_ROW_DETOUR : SAME_ROW_DETOUR);
|
|
1120
|
+
const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
|
|
937
1121
|
return [
|
|
938
1122
|
{
|
|
939
1123
|
x: sx,
|
|
940
1124
|
y: sy
|
|
941
1125
|
},
|
|
942
1126
|
{
|
|
943
|
-
x: sx +
|
|
1127
|
+
x: sx + exitDir,
|
|
944
1128
|
y: sy
|
|
945
1129
|
},
|
|
946
1130
|
{
|
|
947
|
-
x: sx +
|
|
948
|
-
y:
|
|
1131
|
+
x: sx + exitDir,
|
|
1132
|
+
y: detourY
|
|
949
1133
|
},
|
|
950
1134
|
{
|
|
951
|
-
x:
|
|
952
|
-
y:
|
|
1135
|
+
x: approachX,
|
|
1136
|
+
y: detourY
|
|
953
1137
|
},
|
|
954
1138
|
{
|
|
955
|
-
x:
|
|
1139
|
+
x: approachX,
|
|
956
1140
|
y: ty
|
|
957
1141
|
},
|
|
958
1142
|
{
|
|
@@ -962,11 +1146,135 @@ function route(type, src, tgt) {
|
|
|
962
1146
|
];
|
|
963
1147
|
}
|
|
964
1148
|
/**
|
|
1149
|
+
* Routes a link between **different** rows using an orthogonal path.
|
|
1150
|
+
*
|
|
1151
|
+
* 1. Step **away** from the source bar to the crossover x (`crossX`).
|
|
1152
|
+
* 2. Travel **vertically** to the midpoint between rows (`midY`).
|
|
1153
|
+
* 3. Travel **horizontally** to the approach point on the correct side
|
|
1154
|
+
* of the target.
|
|
1155
|
+
* 4. Travel **vertically** to the target row (`ty`).
|
|
1156
|
+
* 5. Final segment to the target entry point (`tx`).
|
|
1157
|
+
*
|
|
1158
|
+
* The crossover x is clamped so the path never doubles back past both bars.
|
|
1159
|
+
* When exit and entry are on the **same** side (SS / FF) the exit-side step
|
|
1160
|
+
* is limited to the approach x, avoiding a wide U‑shape.
|
|
1161
|
+
*
|
|
1162
|
+
* The approach point is chosen so the last segment travels in the arrow
|
|
1163
|
+
* direction demanded by the entry side:
|
|
1164
|
+
*
|
|
1165
|
+
* - Left‑entry (FS, SS): approach from the **left** → `tx - TURN_MARGIN`
|
|
1166
|
+
* Last segment goes RIGHT.
|
|
1167
|
+
* - Right‑entry (FF, SF): approach from the **right** → `tx + TURN_MARGIN`
|
|
1168
|
+
* Last segment goes LEFT.
|
|
1169
|
+
*
|
|
1170
|
+
* left‑entry (FS / SS) right‑entry (FF / SF)
|
|
1171
|
+
* ───────────────────── ─────────────────────
|
|
1172
|
+
* sx ●────────────────► sx ●────────────────►
|
|
1173
|
+
* exitDir exitDir
|
|
1174
|
+
* │ │
|
|
1175
|
+
* │ midY │ midY
|
|
1176
|
+
* ▼ ════════════════════► ▼ ════════════════════►
|
|
1177
|
+
* │ │
|
|
1178
|
+
* │ │
|
|
1179
|
+
* ▼ approachFromLeft ▼ approachFromRight
|
|
1180
|
+
* ●────────────────► ◄────────────────●
|
|
1181
|
+
* tx tx
|
|
1182
|
+
*
|
|
1183
|
+
* @param sx - Source anchor x.
|
|
1184
|
+
* @param sy - Source row center y.
|
|
1185
|
+
* @param tx - Target anchor x.
|
|
1186
|
+
* @param ty - Target row center y.
|
|
1187
|
+
* @param leftEntry - Whether the link enters the target on its left edge.
|
|
1188
|
+
* @param exitRight - Whether the link exits the source on its right edge.
|
|
1189
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1190
|
+
*/
|
|
1191
|
+
function routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight) {
|
|
1192
|
+
const midY = Math.round(Math.abs(sy - ty) / ROW_HEIGHT) % 2 === 0 ? (sy + ty) / 2 + ROW_HEIGHT / 2 : (sy + ty) / 2;
|
|
1193
|
+
const approachX = leftEntry ? tx - TURN_MARGIN : tx + TURN_MARGIN;
|
|
1194
|
+
const crossX = exitRight ? Math.max(sx + TURN_MARGIN, approachX) : Math.min(sx - TURN_MARGIN, approachX);
|
|
1195
|
+
return [
|
|
1196
|
+
{
|
|
1197
|
+
x: sx,
|
|
1198
|
+
y: sy
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
x: crossX,
|
|
1202
|
+
y: sy
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
x: crossX,
|
|
1206
|
+
y: midY
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
x: approachX,
|
|
1210
|
+
y: midY
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
x: approachX,
|
|
1214
|
+
y: ty
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
x: tx,
|
|
1218
|
+
y: ty
|
|
1219
|
+
}
|
|
1220
|
+
];
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Produces the vertex list for an orthogonal connector between source and target.
|
|
1224
|
+
*
|
|
1225
|
+
* ## Anchor points (sx / tx)
|
|
1226
|
+
*
|
|
1227
|
+
* | Type | Source anchor (`sx`) | Target anchor (`tx`) |
|
|
1228
|
+
* |------|-----------------------------|-------------------------------|
|
|
1229
|
+
* | FS | right edge of source | left edge of target |
|
|
1230
|
+
* | SS | left edge of source | left edge of target |
|
|
1231
|
+
* | FF | right edge of source | right edge of target |
|
|
1232
|
+
* | SF | left edge of source | right edge of target |
|
|
1233
|
+
*
|
|
1234
|
+
* Milestone offsets are applied automatically: ± {@link MILESTONE_HALF} replaces
|
|
1235
|
+
* ± width for zero‑width milestones.
|
|
1236
|
+
*
|
|
1237
|
+
* ## Routing strategy
|
|
1238
|
+
*
|
|
1239
|
+
* **Same row** (|sy − ty| < 1 px):
|
|
1240
|
+
* - Direct horizontal line when non‑degenerate **and** the arrow direction
|
|
1241
|
+
* naturally points **into** the target (see {@link routeSameRow}).
|
|
1242
|
+
* - Otherwise a 6‑vertex detour below the bars is drawn.
|
|
1243
|
+
*
|
|
1244
|
+
* **Different rows**: always a 6‑vertex orthogonal path that steps away from
|
|
1245
|
+
* the source, passes through the midpoint between rows, and approaches the
|
|
1246
|
+
* target from the correct side (see {@link routeMultiRow}).
|
|
1247
|
+
*
|
|
1248
|
+
* ## Arrowhead direction guarantee
|
|
1249
|
+
*
|
|
1250
|
+
* The SVG `marker-end` uses `orient="auto"`, so the arrow rotates to match
|
|
1251
|
+
* the last segment. This function ensures the last segment always travels
|
|
1252
|
+
* **into** the target on the semantically correct edge:
|
|
1253
|
+
*
|
|
1254
|
+
* | Entry side | Target edge | Last segment direction |
|
|
1255
|
+
* |-----------|-------------|-----------------------|
|
|
1256
|
+
* | left | left edge | → RIGHT |
|
|
1257
|
+
* | right | right edge | ← LEFT |
|
|
1258
|
+
*
|
|
1259
|
+
* @param type - The link type determining start/end anchor points.
|
|
1260
|
+
* @param src - The source bar layout.
|
|
1261
|
+
* @param tgt - The target bar layout.
|
|
1262
|
+
* @returns An ordered array of {@link Point} vertices.
|
|
1263
|
+
*/
|
|
1264
|
+
function route(type, src, tgt) {
|
|
1265
|
+
const { sx, tx } = getAnchors(type, src, tgt);
|
|
1266
|
+
const sy = src.centerY;
|
|
1267
|
+
const ty = tgt.centerY;
|
|
1268
|
+
const leftEntry = isLeftEntry(type);
|
|
1269
|
+
const exitRight = isExitRight(type);
|
|
1270
|
+
return collapseStubs(Math.abs(sy - ty) < 1 ? routeSameRow(sx, sy, tx, ty, leftEntry, exitRight) : routeMultiRow(sx, sy, tx, ty, leftEntry, exitRight));
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
965
1273
|
* Computes orthogonal routing for all dependency links.
|
|
966
1274
|
* Links whose source or target is not in the layout map are skipped silently
|
|
967
1275
|
* (e.g. when the row is collapsed).
|
|
968
1276
|
*
|
|
969
|
-
* @param links
|
|
1277
|
+
* @param links - The dependency links to route.
|
|
970
1278
|
* @param layouts - A map from task ID to its computed {@link BarLayout}.
|
|
971
1279
|
* @returns An array of {@link RoutedLink} objects with computed vertex paths.
|
|
972
1280
|
*/
|
|
@@ -979,12 +1287,13 @@ function routeLinks(links, layouts) {
|
|
|
979
1287
|
linkId: link.id,
|
|
980
1288
|
sourceTaskId: link.source,
|
|
981
1289
|
targetTaskId: link.target,
|
|
1290
|
+
type: link.type,
|
|
982
1291
|
points: route(link.type, src, tgt)
|
|
983
1292
|
};
|
|
984
1293
|
}).filter((r) => r !== null);
|
|
985
1294
|
}
|
|
986
1295
|
//#endregion
|
|
987
|
-
//#region src/
|
|
1296
|
+
//#region src/lib/vanilla/dom/helpers.ts
|
|
988
1297
|
/**
|
|
989
1298
|
* Batches style assignments; avoids repeated style recalculations.
|
|
990
1299
|
*
|
|
@@ -1030,7 +1339,7 @@ function setAttrs(elem, attrs) {
|
|
|
1030
1339
|
for (const [k, v] of Object.entries(attrs)) elem.setAttribute(k, String(v));
|
|
1031
1340
|
}
|
|
1032
1341
|
//#endregion
|
|
1033
|
-
//#region src/
|
|
1342
|
+
//#region src/lib/vanilla/dom/timeHeader.ts
|
|
1034
1343
|
function specialDayKind(date, specialDaysByDate, showWeekends, weekendDays) {
|
|
1035
1344
|
const dateKey = startOfDay(date).toISOString().slice(0, 10);
|
|
1036
1345
|
const specialDay = specialDaysByDate.get(dateKey);
|
|
@@ -1168,7 +1477,7 @@ function renderTimeHeader(container, state) {
|
|
|
1168
1477
|
container.append(lowerRow);
|
|
1169
1478
|
}
|
|
1170
1479
|
//#endregion
|
|
1171
|
-
//#region src/
|
|
1480
|
+
//#region src/lib/vanilla/dom/gridColumns.ts
|
|
1172
1481
|
const DEFAULT_GRID_COLUMNS = [
|
|
1173
1482
|
{
|
|
1174
1483
|
id: "name",
|
|
@@ -1249,8 +1558,8 @@ function visibleColumns(columns) {
|
|
|
1249
1558
|
return columns.filter((c) => c.visible !== false);
|
|
1250
1559
|
}
|
|
1251
1560
|
const GRID_COLUMN_FR_MIN_WIDTH = 120;
|
|
1252
|
-
const PX_RE = /^(\d+(?:\.\d+)?)px
|
|
1253
|
-
const FR_RE = /^(\d+(?:\.\d+)?)fr
|
|
1561
|
+
const PX_RE = /^(\d+(?:\.\d+)?)px$/u;
|
|
1562
|
+
const FR_RE = /^(\d+(?:\.\d+)?)fr$/u;
|
|
1254
1563
|
function parseColumnMinWidth(width) {
|
|
1255
1564
|
const trimmed = width.trim();
|
|
1256
1565
|
const pxMatch = PX_RE.exec(trimmed);
|
|
@@ -1272,21 +1581,45 @@ function gridNaturalWidth(columns) {
|
|
|
1272
1581
|
return total;
|
|
1273
1582
|
}
|
|
1274
1583
|
//#endregion
|
|
1275
|
-
//#region src/
|
|
1584
|
+
//#region src/lib/vanilla/dom/leftPane.ts
|
|
1276
1585
|
const INDENT = 16;
|
|
1277
1586
|
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 }
|
|
1587
|
+
function toTask$1(node) {
|
|
1588
|
+
const base = {
|
|
1589
|
+
id: node.id,
|
|
1590
|
+
text: node.text,
|
|
1591
|
+
startDate: node.startDate,
|
|
1592
|
+
...node.parent === void 0 ? {} : { parent: node.parent },
|
|
1593
|
+
...node.color === void 0 ? {} : { color: node.color },
|
|
1594
|
+
...node.data === void 0 ? {} : { data: node.data }
|
|
1289
1595
|
};
|
|
1596
|
+
switch (node.kind) {
|
|
1597
|
+
case "task": return {
|
|
1598
|
+
...base,
|
|
1599
|
+
kind: "task",
|
|
1600
|
+
durationHours: node.durationHours,
|
|
1601
|
+
percentComplete: node.percentComplete
|
|
1602
|
+
};
|
|
1603
|
+
case "project": return {
|
|
1604
|
+
...base,
|
|
1605
|
+
kind: "project",
|
|
1606
|
+
durationHours: node.durationHours,
|
|
1607
|
+
percentComplete: node.percentComplete,
|
|
1608
|
+
open: node.open
|
|
1609
|
+
};
|
|
1610
|
+
case "milestone": return {
|
|
1611
|
+
...base,
|
|
1612
|
+
kind: "milestone"
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function getTaskField(task, field) {
|
|
1617
|
+
switch (field) {
|
|
1618
|
+
case "durationHours": return task.kind !== "milestone" ? task.durationHours : void 0;
|
|
1619
|
+
case "percentComplete": return task.kind !== "milestone" ? task.percentComplete : void 0;
|
|
1620
|
+
case "open": return task.kind === "project" ? task.open : void 0;
|
|
1621
|
+
default: return task[field];
|
|
1622
|
+
}
|
|
1290
1623
|
}
|
|
1291
1624
|
function buildTreeNameCell(row, expandedIds, cbs) {
|
|
1292
1625
|
const hasChildren = isParent(row);
|
|
@@ -1330,7 +1663,7 @@ function buildTreeNameCell(row, expandedIds, cbs) {
|
|
|
1330
1663
|
const label = el("span");
|
|
1331
1664
|
css(label, {
|
|
1332
1665
|
fontSize: "var(--gantt-font-size-md)",
|
|
1333
|
-
fontWeight: row.
|
|
1666
|
+
fontWeight: row.kind === "project" ? "var(--gantt-font-weight-bold)" : "var(--gantt-font-weight-normal)",
|
|
1334
1667
|
color: "var(--gantt-text)",
|
|
1335
1668
|
overflow: "hidden",
|
|
1336
1669
|
textOverflow: "ellipsis",
|
|
@@ -1354,9 +1687,9 @@ function buildDataCell(row, column, locale) {
|
|
|
1354
1687
|
css(cell, styles);
|
|
1355
1688
|
const task = toTask$1(row);
|
|
1356
1689
|
if (column.field !== void 0) {
|
|
1357
|
-
const rawValue = task
|
|
1690
|
+
const rawValue = getTaskField(task, column.field);
|
|
1358
1691
|
if (column.format !== void 0) cell.textContent = column.format(rawValue, task, row, locale);
|
|
1359
|
-
else cell.textContent = rawValue !== null && rawValue !== void 0 ? String(rawValue) : "";
|
|
1692
|
+
else cell.textContent = rawValue !== null && rawValue !== void 0 ? typeof rawValue === "object" ? JSON.stringify(rawValue) : String(rawValue) : "";
|
|
1360
1693
|
}
|
|
1361
1694
|
return cell;
|
|
1362
1695
|
}
|
|
@@ -1375,7 +1708,7 @@ function buildAddButton(row, cbs, locale) {
|
|
|
1375
1708
|
});
|
|
1376
1709
|
btn.addEventListener("click", (event) => {
|
|
1377
1710
|
event.stopPropagation();
|
|
1378
|
-
cbs.
|
|
1711
|
+
cbs.onTaskAdd(row.id);
|
|
1379
1712
|
});
|
|
1380
1713
|
return btn;
|
|
1381
1714
|
}
|
|
@@ -1415,7 +1748,7 @@ function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
|
|
|
1415
1748
|
wrapper.addEventListener("keydown", (event) => {
|
|
1416
1749
|
if (event.key === "Enter" || event.key === " ") {
|
|
1417
1750
|
event.preventDefault();
|
|
1418
|
-
cbs.
|
|
1751
|
+
cbs.onTaskSelect(row.id);
|
|
1419
1752
|
}
|
|
1420
1753
|
});
|
|
1421
1754
|
for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
|
|
@@ -1566,23 +1899,22 @@ function setupColumnResize(headerEl, bodyEl, columns, onChange) {
|
|
|
1566
1899
|
};
|
|
1567
1900
|
}
|
|
1568
1901
|
//#endregion
|
|
1569
|
-
//#region src/
|
|
1902
|
+
//#region src/lib/vanilla/dom/dependencyLayer.ts
|
|
1570
1903
|
const NS = "http://www.w3.org/2000/svg";
|
|
1904
|
+
const ARROW_PATH = "M 0 1 L 10 5 L 0 9 Z";
|
|
1571
1905
|
const ARROW_SIZE = 6;
|
|
1572
1906
|
/**
|
|
1573
1907
|
* Creates the SVG overlay element. Call once; pass to updateDependencyLayer on each render.
|
|
1574
1908
|
* Also creates a hidden ghost-line path used during link-creation drags.
|
|
1575
1909
|
*
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1910
|
+
* The SVG is initially zero-sized; `updateDependencyLayer` sets width/height each frame.
|
|
1911
|
+
*
|
|
1912
|
+
* @param _totalWidth - The total pixel width of the SVG viewport.
|
|
1913
|
+
* @param _totalHeight - The total pixel height of the SVG viewport.
|
|
1578
1914
|
* @returns An `SVGSVGElement` ready to be inserted into the DOM.
|
|
1579
1915
|
*/
|
|
1580
|
-
function createDependencyLayer(
|
|
1916
|
+
function createDependencyLayer(_totalWidth, _totalHeight) {
|
|
1581
1917
|
const svg = document.createElementNS(NS, "svg");
|
|
1582
|
-
setAttrs(svg, {
|
|
1583
|
-
width: totalWidth,
|
|
1584
|
-
height: totalHeight
|
|
1585
|
-
});
|
|
1586
1918
|
Object.assign(svg.style, {
|
|
1587
1919
|
position: "absolute",
|
|
1588
1920
|
top: "0",
|
|
@@ -1597,7 +1929,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1597
1929
|
setAttrs(marker, {
|
|
1598
1930
|
id,
|
|
1599
1931
|
viewBox: "0 0 10 10",
|
|
1600
|
-
refX: "
|
|
1932
|
+
refX: "10",
|
|
1601
1933
|
refY: "5",
|
|
1602
1934
|
markerWidth: ARROW_SIZE,
|
|
1603
1935
|
markerHeight: ARROW_SIZE,
|
|
@@ -1605,7 +1937,7 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1605
1937
|
});
|
|
1606
1938
|
const path = document.createElementNS(NS, "path");
|
|
1607
1939
|
setAttrs(path, {
|
|
1608
|
-
d:
|
|
1940
|
+
d: ARROW_PATH,
|
|
1609
1941
|
fill: color
|
|
1610
1942
|
});
|
|
1611
1943
|
marker.append(path);
|
|
@@ -1638,10 +1970,9 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1638
1970
|
function showGhostLine(svg, x1, y1, x2, y2, valid) {
|
|
1639
1971
|
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
1640
1972
|
if (ghost === null) return;
|
|
1641
|
-
setAttrs(ghost, {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
});
|
|
1973
|
+
setAttrs(ghost, { d: `M ${x1},${y1} L ${x2},${y2}` });
|
|
1974
|
+
if (valid) ghost.removeAttribute("stroke-dasharray");
|
|
1975
|
+
else ghost.setAttribute("stroke-dasharray", "5 3");
|
|
1645
1976
|
if (valid) ghost.setAttribute("marker-end", "url(#gantt-arrow)");
|
|
1646
1977
|
else ghost.removeAttribute("marker-end");
|
|
1647
1978
|
ghost.style.display = "";
|
|
@@ -1668,26 +1999,21 @@ function hideGhostLine(svg) {
|
|
|
1668
1999
|
* @param totalHeight - The total pixel height of the SVG viewport.
|
|
1669
2000
|
* @param selectedTaskId - The currently selected task ID, or `null`.
|
|
1670
2001
|
* @param highlightLinkedDependenciesOnSelect - When `true`, links connected to the selected task use highlight styling.
|
|
2002
|
+
* @param cbs - Optional callbacks for link click and double-click events.
|
|
1671
2003
|
*/
|
|
1672
|
-
function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect) {
|
|
2004
|
+
function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect, cbs) {
|
|
1673
2005
|
setAttrs(svg, {
|
|
1674
2006
|
width: totalWidth,
|
|
1675
2007
|
height: totalHeight
|
|
1676
2008
|
});
|
|
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
|
-
}
|
|
2009
|
+
const toRemove = [...svg.children].slice(1).filter((c) => !c.classList.contains("gantt-ghost-line"));
|
|
1682
2010
|
for (const node of toRemove) svg.removeChild(node);
|
|
2011
|
+
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
1683
2012
|
for (const link of links) {
|
|
1684
2013
|
const { points } = link;
|
|
1685
2014
|
if (points.length === 0) continue;
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const p = points[i];
|
|
1689
|
-
if (p !== void 0) d += ` L ${p.x},${p.y}`;
|
|
1690
|
-
}
|
|
2015
|
+
const [first, ...rest] = points;
|
|
2016
|
+
const d = `M ${first.x},${first.y}${rest.map((p) => ` L ${p.x},${p.y}`).join("")}`;
|
|
1691
2017
|
const isRelated = highlightLinkedDependenciesOnSelect && selectedTaskId !== null && (link.sourceTaskId === selectedTaskId || link.targetTaskId === selectedTaskId);
|
|
1692
2018
|
const path = document.createElementNS(NS, "path");
|
|
1693
2019
|
setAttrs(path, {
|
|
@@ -1696,25 +2022,61 @@ function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTask
|
|
|
1696
2022
|
stroke: isRelated ? "var(--gantt-link-hi)" : "var(--gantt-link)",
|
|
1697
2023
|
"stroke-width": isRelated ? "1.8" : "1.5",
|
|
1698
2024
|
"stroke-linejoin": "round",
|
|
1699
|
-
"marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)"
|
|
2025
|
+
"marker-end": isRelated ? "url(#gantt-arrow-hi)" : "url(#gantt-arrow)",
|
|
2026
|
+
"data-link-id": String(link.linkId)
|
|
1700
2027
|
});
|
|
1701
|
-
|
|
2028
|
+
path.style.pointerEvents = "visibleStroke";
|
|
2029
|
+
path.style.cursor = "pointer";
|
|
2030
|
+
path.addEventListener("click", (event) => {
|
|
2031
|
+
if (event.detail === 1) cbs?.onLinkClick?.({
|
|
2032
|
+
id: link.linkId,
|
|
2033
|
+
source: link.sourceTaskId,
|
|
2034
|
+
target: link.targetTaskId,
|
|
2035
|
+
type: link.type
|
|
2036
|
+
});
|
|
2037
|
+
});
|
|
2038
|
+
path.addEventListener("dblclick", (_event) => {
|
|
2039
|
+
cbs?.onLinkDblClick?.({
|
|
2040
|
+
id: link.linkId,
|
|
2041
|
+
source: link.sourceTaskId,
|
|
2042
|
+
target: link.targetTaskId,
|
|
2043
|
+
type: link.type
|
|
2044
|
+
});
|
|
2045
|
+
});
|
|
2046
|
+
if (ghost !== null) svg.insertBefore(path, ghost);
|
|
2047
|
+
else svg.append(path);
|
|
1702
2048
|
}
|
|
1703
2049
|
}
|
|
1704
2050
|
//#endregion
|
|
1705
|
-
//#region src/
|
|
1706
|
-
function toTask(
|
|
1707
|
-
|
|
1708
|
-
id:
|
|
1709
|
-
text:
|
|
1710
|
-
startDate:
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
open: row.open,
|
|
1715
|
-
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1716
|
-
...row.color === void 0 ? {} : { color: row.color }
|
|
2051
|
+
//#region src/lib/vanilla/interaction/drag.ts
|
|
2052
|
+
function toTask(node) {
|
|
2053
|
+
const base = {
|
|
2054
|
+
id: node.id,
|
|
2055
|
+
text: node.text,
|
|
2056
|
+
startDate: node.startDate,
|
|
2057
|
+
...node.parent === void 0 ? {} : { parent: node.parent },
|
|
2058
|
+
...node.color === void 0 ? {} : { color: node.color },
|
|
2059
|
+
...node.data === void 0 ? {} : { data: node.data }
|
|
1717
2060
|
};
|
|
2061
|
+
switch (node.kind) {
|
|
2062
|
+
case "task": return {
|
|
2063
|
+
...base,
|
|
2064
|
+
kind: "task",
|
|
2065
|
+
durationHours: node.durationHours,
|
|
2066
|
+
percentComplete: node.percentComplete
|
|
2067
|
+
};
|
|
2068
|
+
case "project": return {
|
|
2069
|
+
...base,
|
|
2070
|
+
kind: "project",
|
|
2071
|
+
durationHours: node.durationHours,
|
|
2072
|
+
percentComplete: node.percentComplete,
|
|
2073
|
+
open: node.open
|
|
2074
|
+
};
|
|
2075
|
+
case "milestone": return {
|
|
2076
|
+
...base,
|
|
2077
|
+
kind: "milestone"
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
1718
2080
|
}
|
|
1719
2081
|
/**
|
|
1720
2082
|
* Attaches drag-to-move and resize listeners to a bar element.
|
|
@@ -1736,22 +2098,27 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1736
2098
|
try {
|
|
1737
2099
|
barEl.setPointerCapture(e.pointerId);
|
|
1738
2100
|
} catch {}
|
|
1739
|
-
cbs.
|
|
2101
|
+
cbs.onTaskSelect?.(task.id);
|
|
1740
2102
|
const startX = e.clientX;
|
|
1741
2103
|
const originDate = parseDate(task.startDate);
|
|
1742
2104
|
const mapper = getMapper();
|
|
2105
|
+
let lastHours = 0;
|
|
1743
2106
|
function onMove(me) {
|
|
1744
2107
|
const dx = me.clientX - startX;
|
|
1745
|
-
|
|
1746
|
-
cbs.
|
|
2108
|
+
lastHours = Math.round(mapper.widthToDuration(dx));
|
|
2109
|
+
cbs.onTaskMove?.({
|
|
1747
2110
|
id: task.id,
|
|
1748
|
-
startDate: addHours(originDate,
|
|
2111
|
+
startDate: addHours(originDate, lastHours)
|
|
1749
2112
|
});
|
|
1750
2113
|
}
|
|
1751
2114
|
function onUp() {
|
|
1752
2115
|
window.removeEventListener("pointermove", onMove);
|
|
1753
2116
|
window.removeEventListener("pointerup", onUp);
|
|
1754
2117
|
barEl.style.cursor = "grab";
|
|
2118
|
+
cbs._onTaskMoveFinal?.({
|
|
2119
|
+
id: task.id,
|
|
2120
|
+
startDate: addHours(originDate, lastHours)
|
|
2121
|
+
});
|
|
1755
2122
|
}
|
|
1756
2123
|
barEl.style.cursor = "grabbing";
|
|
1757
2124
|
window.addEventListener("pointermove", onMove);
|
|
@@ -1765,29 +2132,33 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1765
2132
|
resizeHandleEl.setPointerCapture(e.pointerId);
|
|
1766
2133
|
} catch {}
|
|
1767
2134
|
const startX = e.clientX;
|
|
1768
|
-
const origDur = task.durationHours;
|
|
2135
|
+
const origDur = task.kind !== "milestone" ? task.durationHours : 0;
|
|
1769
2136
|
const mapper = getMapper();
|
|
2137
|
+
let lastDuration = origDur;
|
|
1770
2138
|
function onMove(me) {
|
|
1771
2139
|
const dx = me.clientX - startX;
|
|
1772
2140
|
const hoursDelta = Math.round(mapper.widthToDuration(dx));
|
|
1773
|
-
|
|
2141
|
+
lastDuration = Math.max(1, origDur + hoursDelta);
|
|
2142
|
+
cbs.onTaskResize?.({
|
|
1774
2143
|
id: task.id,
|
|
1775
|
-
durationHours:
|
|
2144
|
+
durationHours: lastDuration
|
|
1776
2145
|
});
|
|
1777
2146
|
}
|
|
1778
2147
|
function onUp() {
|
|
1779
2148
|
window.removeEventListener("pointermove", onMove);
|
|
1780
2149
|
window.removeEventListener("pointerup", onUp);
|
|
2150
|
+
cbs._onTaskResizeFinal?.({
|
|
2151
|
+
id: task.id,
|
|
2152
|
+
durationHours: lastDuration
|
|
2153
|
+
});
|
|
1781
2154
|
}
|
|
1782
2155
|
window.addEventListener("pointermove", onMove);
|
|
1783
2156
|
window.addEventListener("pointerup", onUp);
|
|
1784
2157
|
}
|
|
1785
2158
|
function onBarClick(event) {
|
|
1786
2159
|
if (event.detail !== 2) return;
|
|
1787
|
-
cbs.
|
|
2160
|
+
cbs.onTaskDoubleClick?.({
|
|
1788
2161
|
id: task.id,
|
|
1789
|
-
source: "bar",
|
|
1790
|
-
trigger: "doubleClick",
|
|
1791
2162
|
task: toTask(task)
|
|
1792
2163
|
});
|
|
1793
2164
|
}
|
|
@@ -1801,6 +2172,56 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1801
2172
|
};
|
|
1802
2173
|
}
|
|
1803
2174
|
/**
|
|
2175
|
+
* Attaches drag-to-change-progress listeners to a progress overlay element.
|
|
2176
|
+
*
|
|
2177
|
+
* @param progressEl - The progress overlay DOM element.
|
|
2178
|
+
* @param barEl - The bar DOM element (for width measurement).
|
|
2179
|
+
* @param task - The {@link TaskNode} for this bar.
|
|
2180
|
+
* @param _getMapper - A function returning the current {@link PixelMapper} (unused, kept for API symmetry).
|
|
2181
|
+
* @param cbs - The chart callbacks.
|
|
2182
|
+
* @returns A cleanup function that removes all listeners.
|
|
2183
|
+
*/
|
|
2184
|
+
function attachProgressDrag(progressEl, barEl, task, _getMapper, cbs) {
|
|
2185
|
+
function onProgressDown(e) {
|
|
2186
|
+
if (e.button !== 0) return;
|
|
2187
|
+
e.preventDefault();
|
|
2188
|
+
e.stopPropagation();
|
|
2189
|
+
cbs.onTaskSelect?.(task.id);
|
|
2190
|
+
try {
|
|
2191
|
+
progressEl.setPointerCapture(e.pointerId);
|
|
2192
|
+
} catch {}
|
|
2193
|
+
const startX = e.clientX;
|
|
2194
|
+
const barWidth = barEl.getBoundingClientRect().width;
|
|
2195
|
+
const origPercent = task.kind !== "milestone" ? task.percentComplete ?? 0 : 0;
|
|
2196
|
+
let lastPercent = origPercent;
|
|
2197
|
+
function onMove(me) {
|
|
2198
|
+
const dx = me.clientX - startX;
|
|
2199
|
+
const percentDelta = barWidth > 0 ? dx / barWidth * 100 : 0;
|
|
2200
|
+
lastPercent = Math.max(0, Math.min(100, Math.round(origPercent + percentDelta)));
|
|
2201
|
+
cbs.onTaskProgressDrag?.({
|
|
2202
|
+
id: task.id,
|
|
2203
|
+
percentComplete: lastPercent
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
function onUp() {
|
|
2207
|
+
window.removeEventListener("pointermove", onMove);
|
|
2208
|
+
window.removeEventListener("pointerup", onUp);
|
|
2209
|
+
progressEl.style.cursor = "ew-resize";
|
|
2210
|
+
cbs._onTaskProgressDragFinal?.({
|
|
2211
|
+
id: task.id,
|
|
2212
|
+
percentComplete: lastPercent
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
progressEl.style.cursor = "ew-resize";
|
|
2216
|
+
window.addEventListener("pointermove", onMove);
|
|
2217
|
+
window.addEventListener("pointerup", onUp);
|
|
2218
|
+
}
|
|
2219
|
+
progressEl.addEventListener("pointerdown", onProgressDown);
|
|
2220
|
+
return () => {
|
|
2221
|
+
progressEl.removeEventListener("pointerdown", onProgressDown);
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
1804
2225
|
* Attaches click-to-select on a milestone diamond.
|
|
1805
2226
|
*
|
|
1806
2227
|
* @param diamondEl - The milestone diamond DOM element.
|
|
@@ -1810,16 +2231,14 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1810
2231
|
*/
|
|
1811
2232
|
function attachMilestoneClick(diamondEl, taskId, cbs) {
|
|
1812
2233
|
function onClick() {
|
|
1813
|
-
cbs.
|
|
2234
|
+
cbs.onTaskSelect?.(taskId);
|
|
1814
2235
|
}
|
|
1815
2236
|
function onDoubleClick(event) {
|
|
1816
2237
|
if (event.detail === 2) {
|
|
1817
2238
|
const task = diamondEl.__task;
|
|
1818
2239
|
if (task === void 0) return;
|
|
1819
|
-
cbs.
|
|
2240
|
+
cbs.onTaskDoubleClick?.({
|
|
1820
2241
|
id: taskId,
|
|
1821
|
-
source: "milestone",
|
|
1822
|
-
trigger: "doubleClick",
|
|
1823
2242
|
task
|
|
1824
2243
|
});
|
|
1825
2244
|
}
|
|
@@ -1835,7 +2254,7 @@ function bindMilestoneTask(diamondEl, task) {
|
|
|
1835
2254
|
diamondEl.__task = task;
|
|
1836
2255
|
}
|
|
1837
2256
|
//#endregion
|
|
1838
|
-
//#region src/
|
|
2257
|
+
//#region src/lib/vanilla/interaction/linkCreation.ts
|
|
1839
2258
|
/**
|
|
1840
2259
|
* Attaches a link-creation drag listener to an endpoint handle.
|
|
1841
2260
|
*
|
|
@@ -1917,7 +2336,7 @@ function createEndpointHandle() {
|
|
|
1917
2336
|
return handle;
|
|
1918
2337
|
}
|
|
1919
2338
|
//#endregion
|
|
1920
|
-
//#region src/
|
|
2339
|
+
//#region src/lib/vanilla/dom/rightPane.ts
|
|
1921
2340
|
const BAR_COLOR = {
|
|
1922
2341
|
task: "var(--gantt-task)",
|
|
1923
2342
|
project: "var(--gantt-project)",
|
|
@@ -1993,7 +2412,7 @@ function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
|
|
|
1993
2412
|
}
|
|
1994
2413
|
function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs) {
|
|
1995
2414
|
const selected = task.id === selectedId;
|
|
1996
|
-
const color = BAR_COLOR[layout.
|
|
2415
|
+
const color = BAR_COLOR[layout.kind] ?? BAR_COLOR["task"];
|
|
1997
2416
|
const bar = el("div");
|
|
1998
2417
|
bar.className = `gantt-bar${selected ? " gantt-bar--selected gantt-shape--selected" : ""}`;
|
|
1999
2418
|
css(bar, {
|
|
@@ -2003,15 +2422,17 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2003
2422
|
width: `${layout.width}px`,
|
|
2004
2423
|
height: `${layout.height}px`,
|
|
2005
2424
|
...color === void 0 ? {} : { background: color },
|
|
2006
|
-
borderRadius: layout.
|
|
2425
|
+
borderRadius: layout.kind === "project" ? "3px" : "4px",
|
|
2007
2426
|
cursor: "grab",
|
|
2008
2427
|
userSelect: "none",
|
|
2009
2428
|
overflow: "hidden",
|
|
2010
2429
|
zIndex: selected ? "3" : "2",
|
|
2011
2430
|
touchAction: "none"
|
|
2012
2431
|
});
|
|
2432
|
+
let cleanupProgressDrag;
|
|
2013
2433
|
if (layout.progressWidth > 0) {
|
|
2014
2434
|
const prog = el("div");
|
|
2435
|
+
const progressEnabled = state.progressDragEnabled;
|
|
2015
2436
|
css(prog, {
|
|
2016
2437
|
position: "absolute",
|
|
2017
2438
|
left: "0",
|
|
@@ -2019,8 +2440,15 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2019
2440
|
width: `${layout.progressWidth}px`,
|
|
2020
2441
|
height: "100%",
|
|
2021
2442
|
background: "rgba(0,0,0,0.18)",
|
|
2022
|
-
|
|
2443
|
+
...progressEnabled ? {
|
|
2444
|
+
cursor: "ew-resize",
|
|
2445
|
+
touchAction: "none"
|
|
2446
|
+
} : { pointerEvents: "none" }
|
|
2023
2447
|
});
|
|
2448
|
+
if (progressEnabled) {
|
|
2449
|
+
prog.className = "gantt-progress-overlay";
|
|
2450
|
+
cleanupProgressDrag = attachProgressDrag(prog, bar, task, () => state.mapper, cbs);
|
|
2451
|
+
}
|
|
2024
2452
|
bar.append(prog);
|
|
2025
2453
|
}
|
|
2026
2454
|
const label = el("span");
|
|
@@ -2047,12 +2475,12 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2047
2475
|
bar.setAttribute("aria-pressed", String(selected));
|
|
2048
2476
|
bar.dataset["taskId"] = String(task.id);
|
|
2049
2477
|
bar.addEventListener("click", () => {
|
|
2050
|
-
cbs.
|
|
2478
|
+
cbs.onTaskSelect?.(task.id);
|
|
2051
2479
|
});
|
|
2052
2480
|
bar.addEventListener("keydown", (event) => {
|
|
2053
2481
|
if (event.key === "Enter" || event.key === " ") {
|
|
2054
2482
|
event.preventDefault();
|
|
2055
|
-
cbs.
|
|
2483
|
+
cbs.onTaskSelect?.(task.id);
|
|
2056
2484
|
}
|
|
2057
2485
|
});
|
|
2058
2486
|
const handle = el("div");
|
|
@@ -2110,6 +2538,7 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
2110
2538
|
cleanupDrag
|
|
2111
2539
|
};
|
|
2112
2540
|
if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
|
|
2541
|
+
if (cleanupProgressDrag !== void 0) entry.cleanupProgressDrag = cleanupProgressDrag;
|
|
2113
2542
|
registry.set(task.id, entry);
|
|
2114
2543
|
}
|
|
2115
2544
|
function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state) {
|
|
@@ -2136,7 +2565,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
2136
2565
|
diamond.addEventListener("keydown", (event) => {
|
|
2137
2566
|
if (event.key === "Enter" || event.key === " ") {
|
|
2138
2567
|
event.preventDefault();
|
|
2139
|
-
cbs.
|
|
2568
|
+
cbs.onTaskSelect?.(task.id);
|
|
2140
2569
|
}
|
|
2141
2570
|
});
|
|
2142
2571
|
const labelEl = el("span");
|
|
@@ -2235,9 +2664,10 @@ function renderRightPane(refs, state, cbs) {
|
|
|
2235
2664
|
for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
|
|
2236
2665
|
for (const node of toRemove) absoluteLayer.removeChild(node);
|
|
2237
2666
|
hideGhostLine(svgLayer);
|
|
2238
|
-
for (const { cleanupDrag, cleanupLinkHandles } of barRegistry.values()) {
|
|
2667
|
+
for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of barRegistry.values()) {
|
|
2239
2668
|
cleanupDrag();
|
|
2240
2669
|
cleanupLinkHandles?.();
|
|
2670
|
+
cleanupProgressDrag?.();
|
|
2241
2671
|
}
|
|
2242
2672
|
barRegistry.clear();
|
|
2243
2673
|
if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
|
|
@@ -2278,13 +2708,13 @@ function renderRightPane(refs, state, cbs) {
|
|
|
2278
2708
|
for (const task of visibleRows) {
|
|
2279
2709
|
const layout = layouts.get(task.id);
|
|
2280
2710
|
if (layout === void 0) continue;
|
|
2281
|
-
if (layout.
|
|
2711
|
+
if (layout.kind === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
|
|
2282
2712
|
else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
|
|
2283
2713
|
}
|
|
2284
|
-
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect);
|
|
2714
|
+
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect, cbs);
|
|
2285
2715
|
}
|
|
2286
2716
|
//#endregion
|
|
2287
|
-
//#region src/
|
|
2717
|
+
//#region src/lib/vanilla/utils.ts
|
|
2288
2718
|
function buildTaskIndex(tasks) {
|
|
2289
2719
|
const index = /* @__PURE__ */ new Map();
|
|
2290
2720
|
for (let i = 0; i < tasks.length; i++) {
|
|
@@ -2333,11 +2763,11 @@ function getExpandableTaskIds(tasks) {
|
|
|
2333
2763
|
function getInitialExpandedIds(tasks) {
|
|
2334
2764
|
const expandableIds = getExpandableTaskIds(tasks);
|
|
2335
2765
|
const expandedIds = /* @__PURE__ */ new Set();
|
|
2336
|
-
for (const task of tasks) if (task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
|
|
2766
|
+
for (const task of tasks) if (task.kind === "project" && task.open && expandableIds.has(task.id)) expandedIds.add(task.id);
|
|
2337
2767
|
return expandedIds;
|
|
2338
2768
|
}
|
|
2339
2769
|
//#endregion
|
|
2340
|
-
//#region src/
|
|
2770
|
+
//#region src/lib/vanilla/splitter.ts
|
|
2341
2771
|
const MIN_PANE_WIDTH$1 = 96;
|
|
2342
2772
|
function attachSplitter(splitterHandle, leftPane, container, timelineMinWidth, onDragEnd) {
|
|
2343
2773
|
splitterHandle.addEventListener("pointerdown", (e) => {
|
|
@@ -2386,7 +2816,7 @@ function computeLeftPaneWidth(options) {
|
|
|
2386
2816
|
return Math.max(MIN_PANE_WIDTH, Math.floor(width));
|
|
2387
2817
|
}
|
|
2388
2818
|
//#endregion
|
|
2389
|
-
//#region src/
|
|
2819
|
+
//#region src/lib/vanilla/gantt-chart.ts
|
|
2390
2820
|
const HEADER_H = 52;
|
|
2391
2821
|
const OVERSCAN = 4;
|
|
2392
2822
|
/**
|
|
@@ -2405,6 +2835,7 @@ const OVERSCAN = 4;
|
|
|
2405
2835
|
var GanttChart = class {
|
|
2406
2836
|
#container;
|
|
2407
2837
|
#opts;
|
|
2838
|
+
#callbacks;
|
|
2408
2839
|
#input = null;
|
|
2409
2840
|
#scale;
|
|
2410
2841
|
#selectedId = null;
|
|
@@ -2412,6 +2843,7 @@ var GanttChart = class {
|
|
|
2412
2843
|
#rafPending = false;
|
|
2413
2844
|
#rafId = null;
|
|
2414
2845
|
#destroyed = false;
|
|
2846
|
+
#dragOriginals = /* @__PURE__ */ new Map();
|
|
2415
2847
|
#taskIndex;
|
|
2416
2848
|
#lastGridClick = null;
|
|
2417
2849
|
#userSplitWidth = null;
|
|
@@ -2426,6 +2858,7 @@ var GanttChart = class {
|
|
|
2426
2858
|
#root;
|
|
2427
2859
|
#scrollEl;
|
|
2428
2860
|
#leftPane;
|
|
2861
|
+
#leftHeader;
|
|
2429
2862
|
#leftBody;
|
|
2430
2863
|
#rightPane;
|
|
2431
2864
|
#rightHeader;
|
|
@@ -2440,10 +2873,11 @@ var GanttChart = class {
|
|
|
2440
2873
|
* @param container - The host `HTMLElement` the chart will be appended to.
|
|
2441
2874
|
* @param opts - Configuration and callback options.
|
|
2442
2875
|
*/
|
|
2443
|
-
constructor(container, opts = {}) {
|
|
2876
|
+
constructor(container, opts = {}, cbs = {}) {
|
|
2444
2877
|
this.#container = container;
|
|
2445
2878
|
this.#scale = opts.scale ?? "day";
|
|
2446
2879
|
this.#opts = opts;
|
|
2880
|
+
this.#callbacks = cbs;
|
|
2447
2881
|
this.#taskIndex = /* @__PURE__ */ new Map();
|
|
2448
2882
|
this.#locale = resolveChartLocale(opts.locale);
|
|
2449
2883
|
this.#columns = opts.gridColumns ?? gridColumnDefaults(this.#locale);
|
|
@@ -2454,41 +2888,115 @@ var GanttChart = class {
|
|
|
2454
2888
|
this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays ?? []);
|
|
2455
2889
|
this.#expandedIds = /* @__PURE__ */ new Set();
|
|
2456
2890
|
this.#cbs = {
|
|
2457
|
-
|
|
2891
|
+
onTaskSelect: (id) => {
|
|
2458
2892
|
if (this.#selectedId === id) return;
|
|
2459
2893
|
this.#selectedId = id;
|
|
2460
|
-
|
|
2894
|
+
if (this.#selectedId !== null) {
|
|
2895
|
+
const task = this.#findTask(this.#selectedId);
|
|
2896
|
+
if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
|
|
2897
|
+
}
|
|
2461
2898
|
this.#scheduleRender();
|
|
2462
2899
|
},
|
|
2463
2900
|
onTaskDoubleClick: (payload) => {
|
|
2464
|
-
|
|
2901
|
+
this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
|
|
2465
2902
|
},
|
|
2466
2903
|
onTaskEditIntent: (payload) => {
|
|
2467
|
-
|
|
2468
|
-
opts.onTaskDoubleClick?.({
|
|
2469
|
-
id: payload.id,
|
|
2470
|
-
source: payload.source
|
|
2471
|
-
});
|
|
2904
|
+
this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
|
|
2472
2905
|
},
|
|
2473
|
-
|
|
2906
|
+
onTaskMove: (payload) => {
|
|
2907
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
2908
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
2909
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
2910
|
+
}
|
|
2474
2911
|
const iso = payload.startDate.toISOString().slice(0, 10);
|
|
2475
2912
|
this.#patchTask(payload.id, { startDate: iso });
|
|
2476
|
-
opts.onMove?.(payload);
|
|
2477
2913
|
this.#scheduleRender();
|
|
2478
2914
|
},
|
|
2479
|
-
|
|
2915
|
+
_onTaskMoveFinal: (payload) => {
|
|
2916
|
+
const task = this.#findTask(payload.id);
|
|
2917
|
+
if (task !== void 0) {
|
|
2918
|
+
if (this.#callbacks.onTaskMove?.({
|
|
2919
|
+
task,
|
|
2920
|
+
newStartDate: payload.startDate
|
|
2921
|
+
}) === false) {
|
|
2922
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
2923
|
+
if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
this.#dragOriginals.clear();
|
|
2927
|
+
this.#scheduleRender();
|
|
2928
|
+
return true;
|
|
2929
|
+
},
|
|
2930
|
+
onTaskResize: (payload) => {
|
|
2931
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
2932
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
2933
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
2934
|
+
}
|
|
2480
2935
|
this.#patchTask(payload.id, { durationHours: payload.durationHours });
|
|
2481
|
-
opts.onResize?.(payload);
|
|
2482
2936
|
this.#scheduleRender();
|
|
2483
2937
|
},
|
|
2938
|
+
_onTaskResizeFinal: (payload) => {
|
|
2939
|
+
const task = this.#findTask(payload.id);
|
|
2940
|
+
if (task !== void 0) {
|
|
2941
|
+
if (this.#callbacks.onTaskResize?.({
|
|
2942
|
+
task,
|
|
2943
|
+
newDurationHours: payload.durationHours
|
|
2944
|
+
}) === false) {
|
|
2945
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
2946
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
this.#dragOriginals.clear();
|
|
2950
|
+
this.#scheduleRender();
|
|
2951
|
+
return true;
|
|
2952
|
+
},
|
|
2953
|
+
onTaskProgressDrag: (payload) => {
|
|
2954
|
+
if (!this.#dragOriginals.has(payload.id)) {
|
|
2955
|
+
const task = this.#input?.tasks.find((t) => t.id === payload.id);
|
|
2956
|
+
if (task !== void 0) this.#dragOriginals.set(payload.id, task);
|
|
2957
|
+
}
|
|
2958
|
+
this.#patchTask(payload.id, { percentComplete: payload.percentComplete });
|
|
2959
|
+
this.#scheduleRender();
|
|
2960
|
+
},
|
|
2961
|
+
_onTaskProgressDragFinal: (payload) => {
|
|
2962
|
+
const task = this.#findTask(payload.id);
|
|
2963
|
+
if (task !== void 0) {
|
|
2964
|
+
if (this.#callbacks.onProgressChange?.({
|
|
2965
|
+
task,
|
|
2966
|
+
newPercentComplete: payload.percentComplete
|
|
2967
|
+
}) === false) {
|
|
2968
|
+
const original = this.#dragOriginals.get(payload.id);
|
|
2969
|
+
if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
this.#dragOriginals.clear();
|
|
2973
|
+
this.#scheduleRender();
|
|
2974
|
+
return true;
|
|
2975
|
+
},
|
|
2976
|
+
onTaskAdd: (parentId) => {
|
|
2977
|
+
const parentTask = this.#findTask(parentId);
|
|
2978
|
+
if (parentTask !== void 0) this.#callbacks.onTaskAdd?.({ parentTask });
|
|
2979
|
+
},
|
|
2484
2980
|
onLeftPaneWidthChange: (width) => {
|
|
2485
|
-
|
|
2981
|
+
this.#callbacks.onLeftPaneWidthChange?.(width);
|
|
2486
2982
|
},
|
|
2487
2983
|
onGridColumnsChange: (updatedColumns) => {
|
|
2488
|
-
|
|
2984
|
+
this.#callbacks.onGridColumnsChange?.(updatedColumns);
|
|
2489
2985
|
},
|
|
2490
2986
|
onLinkCreate: (payload) => {
|
|
2491
|
-
|
|
2987
|
+
const sourceTask = this.#findTask(payload.sourceTaskId);
|
|
2988
|
+
const targetTask = this.#findTask(payload.targetTaskId);
|
|
2989
|
+
if (sourceTask !== void 0 && targetTask !== void 0) this.#callbacks.onLinkCreate?.({
|
|
2990
|
+
type: "FS",
|
|
2991
|
+
sourceTask,
|
|
2992
|
+
targetTask
|
|
2993
|
+
});
|
|
2994
|
+
},
|
|
2995
|
+
onLinkClick: (payload) => {
|
|
2996
|
+
this.#callbacks.onLinkClick?.({ link: payload });
|
|
2997
|
+
},
|
|
2998
|
+
onLinkDblClick: (payload) => {
|
|
2999
|
+
this.#callbacks.onLinkDblClick?.({ link: payload });
|
|
2492
3000
|
}
|
|
2493
3001
|
};
|
|
2494
3002
|
this.#buildDom();
|
|
@@ -2508,9 +3016,9 @@ var GanttChart = class {
|
|
|
2508
3016
|
this.#assertAlive();
|
|
2509
3017
|
validateLinkRefs(newInput.tasks, newInput.links);
|
|
2510
3018
|
detectCycles(newInput.tasks, newInput.links);
|
|
2511
|
-
this.#input = newInput;
|
|
2512
|
-
this.#taskIndex = buildTaskIndex(
|
|
2513
|
-
this.#expandedIds = getInitialExpandedIds(
|
|
3019
|
+
this.#input = structuredClone(newInput);
|
|
3020
|
+
this.#taskIndex = buildTaskIndex(this.#input.tasks);
|
|
3021
|
+
this.#expandedIds = getInitialExpandedIds(this.#input.tasks);
|
|
2514
3022
|
if (this.#rafPending && this.#rafId !== null) {
|
|
2515
3023
|
cancelAnimationFrame(this.#rafId);
|
|
2516
3024
|
this.#rafId = null;
|
|
@@ -2519,15 +3027,59 @@ var GanttChart = class {
|
|
|
2519
3027
|
this.#render();
|
|
2520
3028
|
}
|
|
2521
3029
|
/**
|
|
2522
|
-
*
|
|
3030
|
+
* Merges the supplied options into the current configuration and re-renders
|
|
3031
|
+
* only the panes affected by the changed options.
|
|
2523
3032
|
*
|
|
2524
|
-
* @param
|
|
3033
|
+
* @param opts - A partial {@link GanttOptions} object. Only the keys present
|
|
3034
|
+
* in this parameter are updated; missing keys keep their
|
|
3035
|
+
* previous values.
|
|
2525
3036
|
* @throws {GanttError} When the instance has been destroyed.
|
|
2526
3037
|
*/
|
|
2527
|
-
|
|
3038
|
+
setOptions(opts) {
|
|
2528
3039
|
this.#assertAlive();
|
|
2529
|
-
this.#
|
|
2530
|
-
this.#
|
|
3040
|
+
Object.assign(this.#opts, opts);
|
|
3041
|
+
this.#scale = this.#opts.scale ?? "day";
|
|
3042
|
+
let columnsChanged = false;
|
|
3043
|
+
if (opts.locale !== void 0) {
|
|
3044
|
+
this.#locale = resolveChartLocale(opts.locale);
|
|
3045
|
+
if (this.#opts.gridColumns === void 0) {
|
|
3046
|
+
this.#columns = gridColumnDefaults(this.#locale);
|
|
3047
|
+
this.#leftPaneDefaultWidth = gridNaturalWidth(this.#columns);
|
|
3048
|
+
columnsChanged = true;
|
|
3049
|
+
}
|
|
3050
|
+
if (this.#opts.weekendDays === void 0) this.#weekendDays = normalizeWeekendDays(this.#locale.weekendDays);
|
|
3051
|
+
}
|
|
3052
|
+
if (opts.gridColumns !== void 0) {
|
|
3053
|
+
this.#columns = opts.gridColumns;
|
|
3054
|
+
this.#leftPaneDefaultWidth = this.#opts.leftPaneWidth ?? gridNaturalWidth(this.#columns);
|
|
3055
|
+
columnsChanged = true;
|
|
3056
|
+
}
|
|
3057
|
+
if (columnsChanged && this.#input !== null) this.#rebuildLeftPaneHeader();
|
|
3058
|
+
if (opts.leftPaneWidth !== void 0) this.#leftPaneDefaultWidth = opts.leftPaneWidth;
|
|
3059
|
+
if (opts.height !== void 0) {
|
|
3060
|
+
this.#height = opts.height;
|
|
3061
|
+
this.#root.style.height = `${this.#height}px`;
|
|
3062
|
+
}
|
|
3063
|
+
if (opts.timelineMinWidth !== void 0) {
|
|
3064
|
+
this.#timelineMinWidth = opts.timelineMinWidth;
|
|
3065
|
+
this.#rightPane.style.minWidth = `${this.#timelineMinWidth}px`;
|
|
3066
|
+
}
|
|
3067
|
+
if (opts.weekendDays !== void 0) this.#weekendDays = normalizeWeekendDays(opts.weekendDays);
|
|
3068
|
+
if (opts.specialDays !== void 0) this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays);
|
|
3069
|
+
if (opts.theme !== void 0) this.#applyTheme();
|
|
3070
|
+
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;
|
|
3071
|
+
if (hasLayoutChange) this.#applyResponsivePaneStyles();
|
|
3072
|
+
const hasLeftPaneChange = columnsChanged || opts.locale !== void 0;
|
|
3073
|
+
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;
|
|
3074
|
+
if (!(hasLeftPaneChange || hasRightPaneChange || hasLayoutChange)) return;
|
|
3075
|
+
if (this.#rafPending && this.#rafId !== null) {
|
|
3076
|
+
cancelAnimationFrame(this.#rafId);
|
|
3077
|
+
this.#rafId = null;
|
|
3078
|
+
this.#rafPending = false;
|
|
3079
|
+
}
|
|
3080
|
+
if (hasLeftPaneChange && !hasRightPaneChange) this.#renderGrid();
|
|
3081
|
+
else if (!hasLeftPaneChange && hasRightPaneChange) this.#renderTimeline();
|
|
3082
|
+
else this.#render();
|
|
2531
3083
|
}
|
|
2532
3084
|
/**
|
|
2533
3085
|
* Programmatically selects or deselects a task.
|
|
@@ -2537,8 +3089,12 @@ var GanttChart = class {
|
|
|
2537
3089
|
*/
|
|
2538
3090
|
select(id) {
|
|
2539
3091
|
this.#assertAlive();
|
|
2540
|
-
this.#selectedId =
|
|
2541
|
-
|
|
3092
|
+
if (id === null) this.#selectedId = null;
|
|
3093
|
+
else {
|
|
3094
|
+
const task = this.#input?.tasks.find((t) => t.id === id);
|
|
3095
|
+
if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
|
|
3096
|
+
this.#selectedId = id;
|
|
3097
|
+
}
|
|
2542
3098
|
if (this.#rafPending && this.#rafId !== null) {
|
|
2543
3099
|
cancelAnimationFrame(this.#rafId);
|
|
2544
3100
|
this.#rafId = null;
|
|
@@ -2589,9 +3145,10 @@ var GanttChart = class {
|
|
|
2589
3145
|
else window.removeEventListener("resize", this.#applyResponsivePaneStyles);
|
|
2590
3146
|
if (this.#rafId !== null) cancelAnimationFrame(this.#rafId);
|
|
2591
3147
|
this.#columnResizeCleanup();
|
|
2592
|
-
for (const { cleanupDrag, cleanupLinkHandles } of this.#rightPaneRefs.barRegistry.values()) {
|
|
3148
|
+
for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of this.#rightPaneRefs.barRegistry.values()) {
|
|
2593
3149
|
cleanupDrag();
|
|
2594
3150
|
cleanupLinkHandles?.();
|
|
3151
|
+
cleanupProgressDrag?.();
|
|
2595
3152
|
}
|
|
2596
3153
|
clearChildren(this.#container);
|
|
2597
3154
|
}
|
|
@@ -2606,15 +3163,16 @@ var GanttChart = class {
|
|
|
2606
3163
|
...patch
|
|
2607
3164
|
};
|
|
2608
3165
|
}
|
|
3166
|
+
#findTask(id) {
|
|
3167
|
+
return this.#input?.tasks.find((t) => t.id === id);
|
|
3168
|
+
}
|
|
2609
3169
|
#handleGridClick = (payload) => {
|
|
2610
3170
|
const now = Date.now();
|
|
2611
3171
|
const prev = this.#lastGridClick;
|
|
2612
3172
|
if (prev !== null && prev.id === payload.id && now - prev.atMs <= 350) {
|
|
2613
3173
|
this.#lastGridClick = null;
|
|
2614
|
-
this.#cbs.
|
|
3174
|
+
this.#cbs.onTaskDoubleClick?.({
|
|
2615
3175
|
id: payload.id,
|
|
2616
|
-
source: "grid",
|
|
2617
|
-
trigger: "doubleClick",
|
|
2618
3176
|
task: payload.task
|
|
2619
3177
|
});
|
|
2620
3178
|
return;
|
|
@@ -2623,7 +3181,7 @@ var GanttChart = class {
|
|
|
2623
3181
|
id: payload.id,
|
|
2624
3182
|
atMs: now
|
|
2625
3183
|
};
|
|
2626
|
-
this.#cbs.
|
|
3184
|
+
this.#cbs.onTaskSelect?.(payload.id);
|
|
2627
3185
|
};
|
|
2628
3186
|
#onScroll = () => {
|
|
2629
3187
|
({scrollTop: this.#scrollTop} = this.#scrollEl);
|
|
@@ -2664,6 +3222,7 @@ var GanttChart = class {
|
|
|
2664
3222
|
scale: this.#scale,
|
|
2665
3223
|
highlightLinkedDependenciesOnSelect: this.#opts.highlightLinkedDependenciesOnSelect ?? false,
|
|
2666
3224
|
linkCreationEnabled: this.#opts.linkCreationEnabled ?? false,
|
|
3225
|
+
progressDragEnabled: this.#opts.progressDragEnabled ?? false,
|
|
2667
3226
|
expandedIds: this.#expandedIds,
|
|
2668
3227
|
selectedId: this.#selectedId,
|
|
2669
3228
|
scrollTop: this.#scrollTop,
|
|
@@ -2690,21 +3249,47 @@ var GanttChart = class {
|
|
|
2690
3249
|
if (input === null) return;
|
|
2691
3250
|
const state = this.#computeState(input);
|
|
2692
3251
|
renderTimeHeader(this.#rightHeader, state);
|
|
3252
|
+
this.#renderGridInternal(state);
|
|
3253
|
+
renderRightPane(this.#rightPaneRefs, state, this.#cbs);
|
|
3254
|
+
};
|
|
3255
|
+
#renderGrid = () => {
|
|
3256
|
+
this.#rafPending = false;
|
|
3257
|
+
const input = this.#input;
|
|
3258
|
+
if (input === null) return;
|
|
3259
|
+
this.#renderGridInternal(this.#computeState(input));
|
|
3260
|
+
};
|
|
3261
|
+
#renderGridInternal(state) {
|
|
2693
3262
|
renderLeftPane(this.#leftBody, state, {
|
|
2694
3263
|
onToggle: (id) => {
|
|
2695
3264
|
if (this.#expandedIds.has(id)) this.#expandedIds.delete(id);
|
|
2696
3265
|
else this.#expandedIds.add(id);
|
|
2697
3266
|
this.#scheduleRender();
|
|
2698
3267
|
},
|
|
2699
|
-
|
|
3268
|
+
onTaskSelect: (id) => this.#cbs.onTaskSelect?.(id),
|
|
2700
3269
|
onRowClick: (payload) => {
|
|
2701
3270
|
this.#handleGridClick(payload);
|
|
2702
3271
|
},
|
|
2703
|
-
|
|
2704
|
-
|
|
3272
|
+
onTaskDoubleClick: (payload) => this.#cbs.onTaskDoubleClick?.(payload),
|
|
3273
|
+
onTaskAdd: (id) => this.#cbs.onTaskAdd?.(id)
|
|
2705
3274
|
}, this.#columns);
|
|
3275
|
+
}
|
|
3276
|
+
#renderTimeline = () => {
|
|
3277
|
+
this.#rafPending = false;
|
|
3278
|
+
const input = this.#input;
|
|
3279
|
+
if (input === null) return;
|
|
3280
|
+
const state = this.#computeState(input);
|
|
3281
|
+
renderTimeHeader(this.#rightHeader, state);
|
|
2706
3282
|
renderRightPane(this.#rightPaneRefs, state, this.#cbs);
|
|
2707
3283
|
};
|
|
3284
|
+
#rebuildLeftPaneHeader() {
|
|
3285
|
+
this.#columnResizeCleanup();
|
|
3286
|
+
clearChildren(this.#leftHeader);
|
|
3287
|
+
const headerEl = buildLeftPaneHeader(this.#columns);
|
|
3288
|
+
this.#leftHeader.append(headerEl);
|
|
3289
|
+
this.#columnResizeCleanup = setupColumnResize(headerEl, this.#leftBody, this.#columns, (updated) => {
|
|
3290
|
+
this.#cbs.onGridColumnsChange?.(updated);
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
2708
3293
|
#scheduleRender() {
|
|
2709
3294
|
if (this.#rafPending || this.#destroyed) return;
|
|
2710
3295
|
this.#rafPending = true;
|
|
@@ -2760,6 +3345,7 @@ var GanttChart = class {
|
|
|
2760
3345
|
const headerEl = buildLeftPaneHeader(this.#columns);
|
|
2761
3346
|
leftHeader.append(headerEl);
|
|
2762
3347
|
leftPane.append(leftHeader);
|
|
3348
|
+
this.#leftHeader = leftHeader;
|
|
2763
3349
|
const leftBody = el("div");
|
|
2764
3350
|
leftPane.append(leftBody);
|
|
2765
3351
|
this.#leftBody = leftBody;
|
|
@@ -2807,12 +3393,14 @@ var GanttChart = class {
|
|
|
2807
3393
|
#wireEvents() {
|
|
2808
3394
|
this.#rightPaneRefs.absoluteLayer.addEventListener("click", (event) => {
|
|
2809
3395
|
if (event.target.closest(".gantt-bar, .gantt-milestone, .gantt-resize-handle")) return;
|
|
2810
|
-
this.#
|
|
3396
|
+
this.#selectedId = null;
|
|
3397
|
+
this.#scheduleRender();
|
|
2811
3398
|
});
|
|
2812
3399
|
this.#root.addEventListener("keydown", (event) => {
|
|
2813
3400
|
if (event.key === "Escape" && this.#selectedId !== null) {
|
|
2814
3401
|
event.preventDefault();
|
|
2815
|
-
this.#
|
|
3402
|
+
this.#selectedId = null;
|
|
3403
|
+
this.#scheduleRender();
|
|
2816
3404
|
}
|
|
2817
3405
|
});
|
|
2818
3406
|
this.#scrollEl.addEventListener("scroll", this.#onScroll);
|
|
@@ -2827,6 +3415,6 @@ var GanttChart = class {
|
|
|
2827
3415
|
}
|
|
2828
3416
|
};
|
|
2829
3417
|
//#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,
|
|
3418
|
+
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
3419
|
|
|
2832
3420
|
//# sourceMappingURL=index.mjs.map
|