gantt-renderer 0.1.2 → 0.2.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 +27 -0
- package/README.md +3 -2
- package/dist/index.d.mts +242 -39
- package/dist/index.mjs +790 -420
- package/dist/index.mjs.map +1 -1
- package/dist/styles/gantt.css +27 -2
- package/package.json +90 -91
package/dist/index.mjs
CHANGED
|
@@ -20,43 +20,95 @@ const SpecialDaySchema = z.object({
|
|
|
20
20
|
className: z.string().min(1).optional()
|
|
21
21
|
});
|
|
22
22
|
const TaskSchema = z.object({
|
|
23
|
+
/** Unique positive integer identifier for the task. */
|
|
23
24
|
id: z.number().int().positive(),
|
|
25
|
+
/** Display name / label of the task. */
|
|
24
26
|
text: z.string().min(1),
|
|
25
27
|
/** ISO date: YYYY-MM-DD */
|
|
26
|
-
|
|
27
|
-
/** Duration in
|
|
28
|
-
|
|
28
|
+
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD"),
|
|
29
|
+
/** Duration in hours; 0 = milestone */
|
|
30
|
+
durationHours: z.number().int().min(0),
|
|
31
|
+
/** Optional id of the parent task. When set, this task is a child in the hierarchy. */
|
|
29
32
|
parent: z.number().int().positive().optional(),
|
|
30
|
-
/** 0–
|
|
31
|
-
|
|
33
|
+
/** 0–100 completion percentage (integer) */
|
|
34
|
+
percentComplete: z.number().int().min(0).max(100).default(0),
|
|
35
|
+
/**
|
|
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
|
+
*/
|
|
32
44
|
type: TaskTypeSchema.default("task"),
|
|
33
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Initial expanded state for tree hierarchy.
|
|
47
|
+
* When `false`, children of this task are hidden on initial render.
|
|
48
|
+
* Only relevant for tasks with child tasks.
|
|
49
|
+
*
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
34
52
|
open: z.boolean().default(true),
|
|
53
|
+
/** Optional CSS color value for the task bar. Overrides the default color assignment. */
|
|
35
54
|
color: z.string().optional()
|
|
36
55
|
});
|
|
37
56
|
const LinkSchema = z.object({
|
|
57
|
+
/** Unique positive integer identifier for the dependency link. */
|
|
38
58
|
id: z.number().int().positive(),
|
|
59
|
+
/** The `id` of the predecessor task (the task that drives the dependency). */
|
|
39
60
|
source: z.number().int().positive(),
|
|
61
|
+
/** The `id` of the successor task (the task that depends on the predecessor). */
|
|
40
62
|
target: z.number().int().positive(),
|
|
63
|
+
/**
|
|
64
|
+
* Dependency type.
|
|
65
|
+
*
|
|
66
|
+
* - `'FS'` — Finish-to-start: successor starts after predecessor finishes.
|
|
67
|
+
* - `'SS'` — Start-to-start: successor starts at the same time as predecessor.
|
|
68
|
+
* - `'FF'` — Finish-to-finish: successor finishes at the same time as predecessor.
|
|
69
|
+
* - `'SF'` — Start-to-finish: successor finishes after predecessor starts.
|
|
70
|
+
*
|
|
71
|
+
* @default 'FS'
|
|
72
|
+
*/
|
|
41
73
|
type: LinkTypeSchema.default("FS")
|
|
42
74
|
});
|
|
43
75
|
const GanttInputSchema = z.object({
|
|
76
|
+
/** Array of task objects. At least one task is required. */
|
|
44
77
|
tasks: z.array(TaskSchema).min(1),
|
|
78
|
+
/** Optional array of dependency link objects. Defaults to empty array. */
|
|
45
79
|
links: z.array(LinkSchema).default([])
|
|
46
80
|
});
|
|
47
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Parses raw external data.
|
|
83
|
+
*
|
|
84
|
+
* @param raw - The unvalidated input from the consumer.
|
|
85
|
+
* @returns The parsed and validated {@link GanttInput}.
|
|
86
|
+
* @throws {import('zod').ZodError} On schema validation failure.
|
|
87
|
+
*/
|
|
48
88
|
function parseGanttInput(raw) {
|
|
49
89
|
return GanttInputSchema.parse(raw);
|
|
50
90
|
}
|
|
51
|
-
/**
|
|
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
|
+
*/
|
|
52
97
|
function safeParseGanttInput(raw) {
|
|
53
98
|
const result = GanttInputSchema.safeParse(raw);
|
|
54
99
|
return result.success ? result.data : null;
|
|
55
100
|
}
|
|
56
101
|
//#endregion
|
|
57
102
|
//#region src/gantt-chart/errors.ts
|
|
103
|
+
/**
|
|
104
|
+
* Domain-specific error with a machine-readable {@link GanttErrorCode}.
|
|
105
|
+
*/
|
|
58
106
|
var GanttError = class extends Error {
|
|
59
107
|
code;
|
|
108
|
+
/**
|
|
109
|
+
* @param code - A machine-readable {@link GanttErrorCode} categorising the error.
|
|
110
|
+
* @param message - A human-readable description.
|
|
111
|
+
*/
|
|
60
112
|
constructor(code, message) {
|
|
61
113
|
super(message);
|
|
62
114
|
this.name = "GanttError";
|
|
@@ -68,7 +120,10 @@ var GanttError = class extends Error {
|
|
|
68
120
|
/**
|
|
69
121
|
* Builds a typed tree from a flat task array.
|
|
70
122
|
* Order of tasks[] is irrelevant — parents need not precede children.
|
|
71
|
-
*
|
|
123
|
+
*
|
|
124
|
+
* @param tasks - The flat array of tasks to convert into a tree.
|
|
125
|
+
* @returns Root-level {@link TaskNode} instances with populated `children`.
|
|
126
|
+
* @throws {GanttError} When a task references a `parent` id that does not exist.
|
|
72
127
|
*/
|
|
73
128
|
function buildTaskTree(tasks) {
|
|
74
129
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -97,7 +152,11 @@ function buildTaskTree(tasks) {
|
|
|
97
152
|
}
|
|
98
153
|
/**
|
|
99
154
|
* Flattens a tree into a visible row list.
|
|
100
|
-
* A node's children are included only when its id is in expandedIds
|
|
155
|
+
* A node's children are included only when its id is in `expandedIds`.
|
|
156
|
+
*
|
|
157
|
+
* @param roots - The root-level {@link TaskNode} instances of the tree.
|
|
158
|
+
* @param expandedIds - Set of task IDs whose children should be rendered.
|
|
159
|
+
* @returns A depth-first flattened array of visible {@link TaskNode} items.
|
|
101
160
|
*/
|
|
102
161
|
function flattenTree(roots, expandedIds) {
|
|
103
162
|
const rows = [];
|
|
@@ -108,7 +167,12 @@ function flattenTree(roots, expandedIds) {
|
|
|
108
167
|
for (const root of roots) walk(root);
|
|
109
168
|
return rows;
|
|
110
169
|
}
|
|
111
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Returns `true` when a node has children in the tree.
|
|
172
|
+
*
|
|
173
|
+
* @param node - The {@link TaskNode} to inspect.
|
|
174
|
+
* @returns `true` if `node.children.length > 0`.
|
|
175
|
+
*/
|
|
112
176
|
function isParent(node) {
|
|
113
177
|
return node.children.length > 0;
|
|
114
178
|
}
|
|
@@ -116,8 +180,10 @@ function isParent(node) {
|
|
|
116
180
|
//#region src/gantt-chart/domain/dependencies.ts
|
|
117
181
|
/**
|
|
118
182
|
* Detects circular dependencies in the link graph using DFS tri-colour marking.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
183
|
+
*
|
|
184
|
+
* @param tasks - The task list (used to build the vertex set).
|
|
185
|
+
* @param links - The dependency links defining the directed edges.
|
|
186
|
+
* @throws {GanttError} When a cycle is detected, with a human-readable cycle path.
|
|
121
187
|
*/
|
|
122
188
|
function detectCycles(tasks, links) {
|
|
123
189
|
const adj = /* @__PURE__ */ new Map();
|
|
@@ -155,8 +221,11 @@ function detectCycles(tasks, links) {
|
|
|
155
221
|
for (const id of adj.keys()) if ((color.get(id) ?? WHITE) === WHITE) dfs(id);
|
|
156
222
|
}
|
|
157
223
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
224
|
+
* Validates that every link references existing task IDs.
|
|
225
|
+
*
|
|
226
|
+
* @param tasks - The task list (used as the reference set of valid IDs).
|
|
227
|
+
* @param links - The dependency links to validate.
|
|
228
|
+
* @throws {GanttError} When any link references a non-existent source or target task.
|
|
160
229
|
*/
|
|
161
230
|
function validateLinkRefs(tasks, links) {
|
|
162
231
|
const ids = new Set(tasks.map((t) => t.id));
|
|
@@ -167,76 +236,6 @@ function validateLinkRefs(tasks, links) {
|
|
|
167
236
|
}
|
|
168
237
|
//#endregion
|
|
169
238
|
//#region src/gantt-chart/locale.ts
|
|
170
|
-
const EN_US_LABELS = {
|
|
171
|
-
aria_task: "Task {0}",
|
|
172
|
-
aria_milestone: "Milestone {0}",
|
|
173
|
-
add_subtask_title: "Add subtask",
|
|
174
|
-
column_task_name: "Task name",
|
|
175
|
-
column_start_time: "Start time",
|
|
176
|
-
column_duration: "Duration",
|
|
177
|
-
column_quarter: "Q"
|
|
178
|
-
};
|
|
179
|
-
const CHART_LOCALE_EN_US = {
|
|
180
|
-
code: "en-US",
|
|
181
|
-
labels: EN_US_LABELS,
|
|
182
|
-
weekStartsOn: 0,
|
|
183
|
-
weekNumbering: "iso",
|
|
184
|
-
weekendDays: [0, 6]
|
|
185
|
-
};
|
|
186
|
-
/**
|
|
187
|
-
* Resolves a ChartLocale from either a full ChartLocale object or a BCP 47 string.
|
|
188
|
-
* When given a string, derives weekStartsOn, weekNumbering, and weekendDays from CLDR conventions.
|
|
189
|
-
*/
|
|
190
|
-
function resolveChartLocale(raw) {
|
|
191
|
-
if (raw === void 0) return CHART_LOCALE_EN_US;
|
|
192
|
-
if (typeof raw !== "string") {
|
|
193
|
-
const locale = {
|
|
194
|
-
code: raw.code,
|
|
195
|
-
weekStartsOn: raw.weekStartsOn ?? deriveWeekStartsOn(raw.code),
|
|
196
|
-
weekNumbering: raw.weekNumbering ?? deriveWeekNumbering(raw.code),
|
|
197
|
-
weekendDays: raw.weekendDays ?? deriveWeekendDays(raw.code)
|
|
198
|
-
};
|
|
199
|
-
if (raw.labels !== void 0) locale.labels = raw.labels;
|
|
200
|
-
return locale;
|
|
201
|
-
}
|
|
202
|
-
const code = raw;
|
|
203
|
-
return {
|
|
204
|
-
code,
|
|
205
|
-
weekStartsOn: deriveWeekStartsOn(code),
|
|
206
|
-
weekNumbering: deriveWeekNumbering(code),
|
|
207
|
-
weekendDays: deriveWeekendDays(code)
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
function tryGetWeekInfo(code) {
|
|
211
|
-
try {
|
|
212
|
-
if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
|
|
213
|
-
const locale = new Intl.Locale(code);
|
|
214
|
-
const fn = locale.getWeekInfo;
|
|
215
|
-
if (typeof fn === "function") return fn.call(locale);
|
|
216
|
-
}
|
|
217
|
-
} catch {}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Derives the first day of week (0=Sun, 1=Mon, 6=Sat) from a BCP 47 code.
|
|
221
|
-
* Uses Intl.Locale.getWeekInfo() where available (Chromium, Safari 15.4+),
|
|
222
|
-
* with a CLDR-based fallback table for Firefox and older runtimes.
|
|
223
|
-
*/
|
|
224
|
-
function deriveWeekStartsOn(code) {
|
|
225
|
-
const primary = code.split("-")[0]?.toLowerCase() ?? "en";
|
|
226
|
-
const region = code.split("-")[1]?.toUpperCase();
|
|
227
|
-
if (region !== void 0) {
|
|
228
|
-
const fromRegion = WEEK_START_REGION[region];
|
|
229
|
-
if (fromRegion !== void 0) return fromRegion;
|
|
230
|
-
}
|
|
231
|
-
const fromLang = WEEK_START_LANG[primary];
|
|
232
|
-
if (fromLang !== void 0) return fromLang;
|
|
233
|
-
const info = tryGetWeekInfo(code);
|
|
234
|
-
if (info !== void 0) {
|
|
235
|
-
const day = info.firstDay;
|
|
236
|
-
return day === 7 ? 0 : day;
|
|
237
|
-
}
|
|
238
|
-
return 1;
|
|
239
|
-
}
|
|
240
239
|
const WEEK_START_REGION = {
|
|
241
240
|
US: 0,
|
|
242
241
|
CA: 0,
|
|
@@ -289,24 +288,6 @@ const WEEK_START_LANG = {
|
|
|
289
288
|
ar: 6,
|
|
290
289
|
fa: 6
|
|
291
290
|
};
|
|
292
|
-
/**
|
|
293
|
-
* Derives the week numbering scheme from a BCP 47 code.
|
|
294
|
-
* Europe and ISO-aligned regions default to 'iso'; Americas and others to 'us'.
|
|
295
|
-
*/
|
|
296
|
-
function deriveWeekNumbering(code) {
|
|
297
|
-
const region = code.split("-")[1]?.toUpperCase();
|
|
298
|
-
if (region !== void 0) {
|
|
299
|
-
const fromRegion = WEEK_NUMBERING_REGION[region];
|
|
300
|
-
if (fromRegion !== void 0) return fromRegion;
|
|
301
|
-
if (region in WEEK_START_REGION) return "us";
|
|
302
|
-
}
|
|
303
|
-
const info = tryGetWeekInfo(code);
|
|
304
|
-
if (info !== void 0) {
|
|
305
|
-
if (info.minimalDays >= 4 && info.firstDay === 1) return "iso";
|
|
306
|
-
return "us";
|
|
307
|
-
}
|
|
308
|
-
return "iso";
|
|
309
|
-
}
|
|
310
291
|
const WEEK_NUMBERING_REGION = {
|
|
311
292
|
US: "us",
|
|
312
293
|
CA: "us",
|
|
@@ -330,28 +311,6 @@ const WEEK_NUMBERING_REGION = {
|
|
|
330
311
|
AU: "us",
|
|
331
312
|
NZ: "us"
|
|
332
313
|
};
|
|
333
|
-
/**
|
|
334
|
-
* Derives weekend days (0=Sun … 6=Sat) from a BCP 47 code.
|
|
335
|
-
* Uses Intl.Locale.getWeekInfo() where available, with a CLDR-based fallback table.
|
|
336
|
-
*/
|
|
337
|
-
function deriveWeekendDays(code) {
|
|
338
|
-
const region = code.split("-")[1]?.toUpperCase();
|
|
339
|
-
if (region !== void 0) {
|
|
340
|
-
const fromRegion = WEEKEND_REGION[region];
|
|
341
|
-
if (fromRegion !== void 0) {
|
|
342
|
-
const days = [...fromRegion];
|
|
343
|
-
days.sort((a, b) => a - b);
|
|
344
|
-
return days;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const info = tryGetWeekInfo(code);
|
|
348
|
-
if (info !== void 0) {
|
|
349
|
-
const days = info.weekend.map((d) => d === 7 ? 0 : d);
|
|
350
|
-
days.sort((a, b) => a - b);
|
|
351
|
-
return days;
|
|
352
|
-
}
|
|
353
|
-
return [0, 6];
|
|
354
|
-
}
|
|
355
314
|
const WEEKEND_REGION = {
|
|
356
315
|
AE: [5, 6],
|
|
357
316
|
AF: [4, 5],
|
|
@@ -385,19 +344,127 @@ const WEEKEND_REGION = {
|
|
|
385
344
|
SO: [5],
|
|
386
345
|
MY: [5, 0]
|
|
387
346
|
};
|
|
347
|
+
const EN_US_LABELS = {
|
|
348
|
+
ariaTask: "Task {0}",
|
|
349
|
+
ariaMilestone: "Milestone {0}",
|
|
350
|
+
addSubtaskTitle: "Add subtask",
|
|
351
|
+
columnTaskName: "Task name",
|
|
352
|
+
columnStartDate: "Start time",
|
|
353
|
+
columnDuration: "Duration",
|
|
354
|
+
columnQuarter: "Q"
|
|
355
|
+
};
|
|
356
|
+
const CHART_LOCALE_EN_US = {
|
|
357
|
+
code: "en-US",
|
|
358
|
+
labels: EN_US_LABELS,
|
|
359
|
+
weekStartsOn: 0,
|
|
360
|
+
weekNumbering: "iso",
|
|
361
|
+
weekendDays: [0, 6]
|
|
362
|
+
};
|
|
363
|
+
function tryGetWeekInfo(code) {
|
|
364
|
+
try {
|
|
365
|
+
if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
|
|
366
|
+
const locale = new Intl.Locale(code);
|
|
367
|
+
const fn = locale.getWeekInfo;
|
|
368
|
+
if (typeof fn === "function") return fn.call(locale);
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
}
|
|
388
372
|
/**
|
|
389
|
-
*
|
|
373
|
+
* Derives the first day of week (0=Sun, 1=Mon, 6=Sat) from a BCP 47 code.
|
|
374
|
+
* Uses `Intl.Locale.getWeekInfo()` where available (Chromium, Safari 15.4+),
|
|
375
|
+
* with a CLDR-based fallback table for Firefox and older runtimes.
|
|
390
376
|
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* - `'simple'`: `Math.ceil(dayOfYear / 7)`.
|
|
377
|
+
* @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
|
|
378
|
+
* @returns The first day of the week: `0` (Sunday), `1` (Monday), or `6` (Saturday).
|
|
394
379
|
*/
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
380
|
+
function deriveWeekStartsOn(code) {
|
|
381
|
+
const primary = code.split("-")[0]?.toLowerCase() ?? "en";
|
|
382
|
+
const region = code.split("-")[1]?.toUpperCase();
|
|
383
|
+
if (region !== void 0) {
|
|
384
|
+
const fromRegion = WEEK_START_REGION[region];
|
|
385
|
+
if (fromRegion !== void 0) return fromRegion;
|
|
386
|
+
}
|
|
387
|
+
const fromLang = WEEK_START_LANG[primary];
|
|
388
|
+
if (fromLang !== void 0) return fromLang;
|
|
389
|
+
const info = tryGetWeekInfo(code);
|
|
390
|
+
if (info !== void 0) {
|
|
391
|
+
const day = info.firstDay;
|
|
392
|
+
return day === 7 ? 0 : day;
|
|
393
|
+
}
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Derives the week numbering scheme from a BCP 47 code.
|
|
398
|
+
* Europe and ISO-aligned regions default to `'iso'`; Americas and others to `'us'`.
|
|
399
|
+
*
|
|
400
|
+
* @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
|
|
401
|
+
* @returns The week numbering scheme: `'iso'`, `'us'`, or `'simple'`.
|
|
402
|
+
*/
|
|
403
|
+
function deriveWeekNumbering(code) {
|
|
404
|
+
const region = code.split("-")[1]?.toUpperCase();
|
|
405
|
+
if (region !== void 0) {
|
|
406
|
+
const fromRegion = WEEK_NUMBERING_REGION[region];
|
|
407
|
+
if (fromRegion !== void 0) return fromRegion;
|
|
408
|
+
if (region in WEEK_START_REGION) return "us";
|
|
409
|
+
}
|
|
410
|
+
const info = tryGetWeekInfo(code);
|
|
411
|
+
if (info !== void 0) {
|
|
412
|
+
if (info.minimalDays >= 4 && info.firstDay === 1) return "iso";
|
|
413
|
+
return "us";
|
|
414
|
+
}
|
|
415
|
+
return "iso";
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Derives weekend days (0=Sun … 6=Sat) from a BCP 47 code.
|
|
419
|
+
* Uses `Intl.Locale.getWeekInfo()` where available, with a CLDR-based fallback table.
|
|
420
|
+
*
|
|
421
|
+
* @param code - A BCP 47 language tag (e.g. `'en-US'`, `'de-DE'`).
|
|
422
|
+
* @returns An array of weekend day indices (sorted ascending).
|
|
423
|
+
*/
|
|
424
|
+
function deriveWeekendDays(code) {
|
|
425
|
+
const region = code.split("-")[1]?.toUpperCase();
|
|
426
|
+
if (region !== void 0) {
|
|
427
|
+
const fromRegion = WEEKEND_REGION[region];
|
|
428
|
+
if (fromRegion !== void 0) {
|
|
429
|
+
const days = [...fromRegion];
|
|
430
|
+
days.sort((a, b) => a - b);
|
|
431
|
+
return days;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const info = tryGetWeekInfo(code);
|
|
435
|
+
if (info !== void 0) {
|
|
436
|
+
const days = info.weekend.map((d) => d === 7 ? 0 : d);
|
|
437
|
+
days.sort((a, b) => a - b);
|
|
438
|
+
return days;
|
|
439
|
+
}
|
|
440
|
+
return [0, 6];
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Resolves a {@link ChartLocale} from either a full `ChartLocale` object or a BCP 47 string.
|
|
444
|
+
* When given a string, derives `weekStartsOn`, `weekNumbering`, and `weekendDays` from CLDR conventions.
|
|
445
|
+
*
|
|
446
|
+
* @param raw - A {@link ChartLocale} object, a BCP 47 language tag string, or `undefined`.
|
|
447
|
+
* @returns A fully resolved {@link ChartLocale} with defaults applied.
|
|
448
|
+
*/
|
|
449
|
+
function resolveChartLocale(raw) {
|
|
450
|
+
if (raw === void 0) return CHART_LOCALE_EN_US;
|
|
451
|
+
if (typeof raw !== "string") {
|
|
452
|
+
const locale = {
|
|
453
|
+
code: raw.code,
|
|
454
|
+
weekStartsOn: raw.weekStartsOn ?? deriveWeekStartsOn(raw.code),
|
|
455
|
+
weekNumbering: raw.weekNumbering ?? deriveWeekNumbering(raw.code),
|
|
456
|
+
weekendDays: raw.weekendDays ?? deriveWeekendDays(raw.code)
|
|
457
|
+
};
|
|
458
|
+
if (raw.labels !== void 0) locale.labels = raw.labels;
|
|
459
|
+
return locale;
|
|
400
460
|
}
|
|
461
|
+
const code = raw;
|
|
462
|
+
return {
|
|
463
|
+
code,
|
|
464
|
+
weekStartsOn: deriveWeekStartsOn(code),
|
|
465
|
+
weekNumbering: deriveWeekNumbering(code),
|
|
466
|
+
weekendDays: deriveWeekendDays(code)
|
|
467
|
+
};
|
|
401
468
|
}
|
|
402
469
|
function isoWeek(date) {
|
|
403
470
|
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
@@ -422,33 +489,107 @@ function simpleWeek(date) {
|
|
|
422
489
|
return Math.ceil((dayOfYear + 1) / 7);
|
|
423
490
|
}
|
|
424
491
|
/**
|
|
492
|
+
* Formats a week number according to the specified scheme.
|
|
493
|
+
*
|
|
494
|
+
* - `'iso'`: ISO 8601 (week 1 contains the first Thursday; Monday start).
|
|
495
|
+
* - `'us'`: Week 1 contains January 1; Sunday start.
|
|
496
|
+
* - `'simple'`: `Math.ceil(dayOfYear / 7)`.
|
|
497
|
+
*
|
|
498
|
+
* @param date - The date to compute the week number for.
|
|
499
|
+
* @param scheme - The week numbering scheme: `'iso'`, `'us'`, or `'simple'`.
|
|
500
|
+
* @returns The week number as a positive integer.
|
|
501
|
+
*/
|
|
502
|
+
function formatWeekNumber(date, scheme) {
|
|
503
|
+
switch (scheme) {
|
|
504
|
+
case "iso": return isoWeek(date);
|
|
505
|
+
case "us": return usWeek(date);
|
|
506
|
+
case "simple": return simpleWeek(date);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
425
510
|
* Formats a label template by replacing `{0}` with the given argument.
|
|
511
|
+
*
|
|
512
|
+
* @param template - The template string containing `{0}` as placeholder.
|
|
513
|
+
* @param arg - The value to substitute for `{0}`.
|
|
514
|
+
* @returns The formatted string with the placeholder replaced.
|
|
426
515
|
*/
|
|
427
516
|
function formatLabel(template, arg) {
|
|
428
517
|
return template.replaceAll("{0}", arg);
|
|
429
518
|
}
|
|
430
519
|
//#endregion
|
|
431
520
|
//#region src/gantt-chart/domain/dateMath.ts
|
|
432
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Parses `YYYY-MM-DD` → UTC midnight `Date`.
|
|
523
|
+
*
|
|
524
|
+
* @param dateStr - An ISO-8601 date string in `YYYY-MM-DD` format.
|
|
525
|
+
* @returns A `Date` representing UTC midnight of the given date.
|
|
526
|
+
* @throws {Error} When `dateStr` does not represent a valid date.
|
|
527
|
+
*/
|
|
433
528
|
function parseDate(dateStr) {
|
|
434
529
|
const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00.000Z`);
|
|
435
530
|
if (isNaN(d.getTime())) throw new Error(`Invalid date: "${dateStr}"`);
|
|
436
531
|
return d;
|
|
437
532
|
}
|
|
438
|
-
/**
|
|
533
|
+
/**
|
|
534
|
+
* Returns `date + n` days using exact millisecond arithmetic.
|
|
535
|
+
*
|
|
536
|
+
* @param date - The base date.
|
|
537
|
+
* @param days - Number of days to add (may be negative).
|
|
538
|
+
* @returns A new `Date` offset by the given number of days.
|
|
539
|
+
*/
|
|
439
540
|
function addDays(date, days) {
|
|
440
541
|
return new Date(date.getTime() + days * 864e5);
|
|
441
542
|
}
|
|
442
|
-
/**
|
|
543
|
+
/**
|
|
544
|
+
* Returns `date + n` hours using exact millisecond arithmetic.
|
|
545
|
+
*
|
|
546
|
+
* @param date - The base date.
|
|
547
|
+
* @param hours - Number of hours to add (may be negative).
|
|
548
|
+
* @returns A new `Date` offset by the given number of hours.
|
|
549
|
+
*/
|
|
550
|
+
function addHours(date, hours) {
|
|
551
|
+
return new Date(date.getTime() + hours * 36e5);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Difference in days (float). Positive when `b > a`.
|
|
555
|
+
*
|
|
556
|
+
* @param a - The earlier date.
|
|
557
|
+
* @param b - The later date.
|
|
558
|
+
* @returns The fractional number of days between the two dates.
|
|
559
|
+
*/
|
|
443
560
|
function diffDays(a, b) {
|
|
444
561
|
return (b.getTime() - a.getTime()) / 864e5;
|
|
445
562
|
}
|
|
446
|
-
/**
|
|
563
|
+
/**
|
|
564
|
+
* Difference in hours (float). Positive when `b > a`.
|
|
565
|
+
*
|
|
566
|
+
* @param a - The earlier date.
|
|
567
|
+
* @param b - The later date.
|
|
568
|
+
* @returns The fractional number of hours between the two dates.
|
|
569
|
+
*/
|
|
570
|
+
function diffHours(a, b) {
|
|
571
|
+
return (b.getTime() - a.getTime()) / 36e5;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Returns the UTC start-of-day for the given date.
|
|
575
|
+
*
|
|
576
|
+
* @param date - Any `Date`.
|
|
577
|
+
* @returns A new `Date` set to UTC midnight of the same calendar date.
|
|
578
|
+
*/
|
|
447
579
|
function startOfDay(date) {
|
|
448
580
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
449
581
|
}
|
|
582
|
+
function resolveQuarterLabel(locale) {
|
|
583
|
+
if (locale.labels?.columnQuarter !== void 0) return locale.labels.columnQuarter;
|
|
584
|
+
return EN_US_LABELS.columnQuarter;
|
|
585
|
+
}
|
|
450
586
|
/**
|
|
451
|
-
* Formats a Date for the time-header label given the active scale.
|
|
587
|
+
* Formats a `Date` for the time-header label given the active scale.
|
|
588
|
+
*
|
|
589
|
+
* @param date - The date to format.
|
|
590
|
+
* @param scale - The active {@link TimeScale} determining the label granularity.
|
|
591
|
+
* @param locale - The {@link ChartLocale} used for formatting.
|
|
592
|
+
* @returns A human-readable header label string.
|
|
452
593
|
*/
|
|
453
594
|
function formatHeaderLabel(date, scale, locale) {
|
|
454
595
|
const { code, weekNumbering: weekNumScheme = "iso" } = locale;
|
|
@@ -473,7 +614,12 @@ function formatHeaderLabel(date, scale, locale) {
|
|
|
473
614
|
}
|
|
474
615
|
/**
|
|
475
616
|
* Returns the upper-level (month/year) label for a given scale column.
|
|
476
|
-
* Used in the top header row.
|
|
617
|
+
* Used in the top header row of the timeline.
|
|
618
|
+
*
|
|
619
|
+
* @param date - The date to format.
|
|
620
|
+
* @param scale - The active {@link TimeScale}. Determines how the upper label is computed.
|
|
621
|
+
* @param locale - The {@link ChartLocale} used for formatting.
|
|
622
|
+
* @returns A human-readable upper-level header label string.
|
|
477
623
|
*/
|
|
478
624
|
function formatUpperLabel(date, scale, locale) {
|
|
479
625
|
const { code } = locale;
|
|
@@ -495,11 +641,13 @@ function formatUpperLabel(date, scale, locale) {
|
|
|
495
641
|
case "year": return `${date.getUTCFullYear()}`;
|
|
496
642
|
}
|
|
497
643
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Formats a `YYYY-MM-DD` string for display in the grid.
|
|
646
|
+
*
|
|
647
|
+
* @param dateStr - An ISO-8601 date string in `YYYY-MM-DD` format.
|
|
648
|
+
* @param locale - The {@link ChartLocale} used for locale-aware formatting.
|
|
649
|
+
* @returns A locale-formatted date string.
|
|
650
|
+
*/
|
|
503
651
|
function formatDisplayDate(dateStr, locale) {
|
|
504
652
|
return parseDate(dateStr).toLocaleDateString(locale.code, {
|
|
505
653
|
year: "numeric",
|
|
@@ -547,8 +695,12 @@ const SCALE_CONFIGS = {
|
|
|
547
695
|
/**
|
|
548
696
|
* Snaps a date to the column boundary for the provided scale.
|
|
549
697
|
* All operations use UTC semantics.
|
|
550
|
-
* The week boundary respects the optional weekStartsOn override (0=Sun, 1=Mon, 6=Sat).
|
|
551
|
-
*
|
|
698
|
+
* The week boundary respects the optional `weekStartsOn` override (0=Sun, 1=Mon, 6=Sat).
|
|
699
|
+
*
|
|
700
|
+
* @param date - The date to snap.
|
|
701
|
+
* @param scale - The target {@link TimeScale}.
|
|
702
|
+
* @param weekStartsOn - First day of the week (`0`-Sun, `1`-Mon, `6`-Sat). Defaults to `1` (Monday).
|
|
703
|
+
* @returns A new `Date` snapped to the column boundary.
|
|
552
704
|
*/
|
|
553
705
|
function snapToScaleBoundary(date, scale, weekStartsOn = 1) {
|
|
554
706
|
switch (scale) {
|
|
@@ -572,6 +724,10 @@ function snapToScaleBoundary(date, scale, weekStartsOn = 1) {
|
|
|
572
724
|
/**
|
|
573
725
|
* Returns the next column boundary from a boundary-aligned date.
|
|
574
726
|
* Month/quarter/year use true calendar stepping (not fixed-day approximations).
|
|
727
|
+
*
|
|
728
|
+
* @param date - A boundary-aligned date.
|
|
729
|
+
* @param scale - The target {@link TimeScale}.
|
|
730
|
+
* @returns The next boundary date.
|
|
575
731
|
*/
|
|
576
732
|
function nextScaleBoundary(date, scale) {
|
|
577
733
|
switch (scale) {
|
|
@@ -588,13 +744,17 @@ function nextScaleBoundary(date, scale) {
|
|
|
588
744
|
/**
|
|
589
745
|
* Creates a stateless pixel mapper for the given scale and viewport start.
|
|
590
746
|
* All conversions are O(1) arithmetic — safe to call in tight loops.
|
|
747
|
+
*
|
|
748
|
+
* @param scale - The active {@link TimeScale}.
|
|
749
|
+
* @param viewportStart - The leftmost date visible in the viewport.
|
|
750
|
+
* @returns A {@link PixelMapper} configured for the given viewport.
|
|
591
751
|
*/
|
|
592
752
|
function createPixelMapper(scale, viewportStart) {
|
|
593
753
|
const { columnWidth, msPerColumn } = SCALE_CONFIGS[scale];
|
|
594
754
|
const originMs = viewportStart.getTime();
|
|
595
755
|
const pxPerMs = columnWidth / msPerColumn;
|
|
596
756
|
const msPerPx = msPerColumn / columnWidth;
|
|
597
|
-
const
|
|
757
|
+
const msPerHour = 36e5;
|
|
598
758
|
return {
|
|
599
759
|
originMs,
|
|
600
760
|
columnWidth,
|
|
@@ -604,11 +764,11 @@ function createPixelMapper(scale, viewportStart) {
|
|
|
604
764
|
toDate(x) {
|
|
605
765
|
return new Date(originMs + x * msPerPx);
|
|
606
766
|
},
|
|
607
|
-
durationToWidth(
|
|
608
|
-
return
|
|
767
|
+
durationToWidth(hours) {
|
|
768
|
+
return hours * msPerHour * pxPerMs;
|
|
609
769
|
},
|
|
610
770
|
widthToDuration(px) {
|
|
611
|
-
return px * msPerPx /
|
|
771
|
+
return px * msPerPx / msPerHour;
|
|
612
772
|
}
|
|
613
773
|
};
|
|
614
774
|
}
|
|
@@ -628,13 +788,17 @@ const MILESTONE_HALF = MILESTONE_SIZE / 2;
|
|
|
628
788
|
/**
|
|
629
789
|
* Computes pixel-space layout for all visible task rows.
|
|
630
790
|
* Returns a map keyed by task id for O(1) lookup during link routing.
|
|
791
|
+
*
|
|
792
|
+
* @param rows - The flattened, visible {@link TaskNode} rows.
|
|
793
|
+
* @param mapper - The {@link PixelMapper} for coordinate conversion.
|
|
794
|
+
* @returns A `Map` from task ID to its computed {@link BarLayout}.
|
|
631
795
|
*/
|
|
632
796
|
function computeLayout(rows, mapper) {
|
|
633
797
|
const result = /* @__PURE__ */ new Map();
|
|
634
798
|
for (let i = 0; i < rows.length; i++) {
|
|
635
799
|
const task = rows[i];
|
|
636
800
|
if (task === void 0) continue;
|
|
637
|
-
const start = parseDate(task.
|
|
801
|
+
const start = parseDate(task.startDate);
|
|
638
802
|
const x = mapper.toX(start);
|
|
639
803
|
const y = i * ROW_HEIGHT + BAR_Y_OFFSET;
|
|
640
804
|
const centerY = i * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
@@ -654,8 +818,8 @@ function computeLayout(rows, mapper) {
|
|
|
654
818
|
});
|
|
655
819
|
continue;
|
|
656
820
|
}
|
|
657
|
-
const width = Math.max(mapper.durationToWidth(task.
|
|
658
|
-
const progressWidth = width * Math.min(1, Math.max(0, task.
|
|
821
|
+
const width = Math.max(mapper.durationToWidth(task.durationHours), 4);
|
|
822
|
+
const progressWidth = width * Math.min(1, Math.max(0, (task.percentComplete ?? 0) / 100));
|
|
659
823
|
result.set(task.id, {
|
|
660
824
|
taskId: task.id,
|
|
661
825
|
x,
|
|
@@ -673,51 +837,39 @@ function computeLayout(rows, mapper) {
|
|
|
673
837
|
}
|
|
674
838
|
/**
|
|
675
839
|
* Computes the total pixel height of all rows.
|
|
840
|
+
*
|
|
841
|
+
* @param rowCount - The number of visible rows.
|
|
842
|
+
* @returns The total pixel height (`rowCount * ROW_HEIGHT`).
|
|
676
843
|
*/
|
|
677
844
|
function totalContentHeight(rowCount) {
|
|
678
845
|
return rowCount * ROW_HEIGHT;
|
|
679
846
|
}
|
|
680
847
|
/**
|
|
681
848
|
* Derives viewport bounds from task data with padding.
|
|
682
|
-
*
|
|
849
|
+
*
|
|
850
|
+
* @param tasks - The task nodes to derive bounds from.
|
|
851
|
+
* @param paddingHours - Extra hours added before the earliest start and after the latest end. Defaults to `48`.
|
|
852
|
+
* @returns A tuple `[start, end]` of UTC midnight `Date` instances.
|
|
683
853
|
*/
|
|
684
|
-
function deriveViewport(tasks,
|
|
854
|
+
function deriveViewport(tasks, paddingHours = 48) {
|
|
685
855
|
if (tasks.length === 0) {
|
|
686
856
|
const now = /* @__PURE__ */ new Date();
|
|
687
|
-
return [now,
|
|
857
|
+
return [now, addHours(now, 720)];
|
|
688
858
|
}
|
|
689
859
|
let minMs = Infinity;
|
|
690
860
|
let maxMs = -Infinity;
|
|
691
861
|
for (const task of tasks) {
|
|
692
|
-
const start = parseDate(task.
|
|
693
|
-
const end =
|
|
862
|
+
const start = parseDate(task.startDate);
|
|
863
|
+
const end = addHours(start, task.durationHours);
|
|
694
864
|
if (start.getTime() < minMs) minMs = start.getTime();
|
|
695
865
|
if (end.getTime() > maxMs) maxMs = end.getTime();
|
|
696
866
|
}
|
|
697
|
-
return [
|
|
867
|
+
return [addHours(new Date(minMs), -paddingHours), addHours(new Date(maxMs), paddingHours)];
|
|
698
868
|
}
|
|
699
869
|
//#endregion
|
|
700
870
|
//#region src/gantt-chart/rendering/linkRouter.ts
|
|
701
871
|
const TURN_MARGIN = 12;
|
|
702
872
|
/**
|
|
703
|
-
* Computes orthogonal routing for all dependency links.
|
|
704
|
-
* Links whose source or target is not in the layout map are skipped silently
|
|
705
|
-
* (e.g. when the row is collapsed).
|
|
706
|
-
*/
|
|
707
|
-
function routeLinks(links, layouts) {
|
|
708
|
-
return links.map((link) => {
|
|
709
|
-
const src = layouts.get(link.source);
|
|
710
|
-
const tgt = layouts.get(link.target);
|
|
711
|
-
if (src === void 0 || tgt === void 0) return null;
|
|
712
|
-
return {
|
|
713
|
-
linkId: link.id,
|
|
714
|
-
sourceTaskId: link.source,
|
|
715
|
-
targetTaskId: link.target,
|
|
716
|
-
points: route(link.type, src, tgt)
|
|
717
|
-
};
|
|
718
|
-
}).filter((r) => r !== null);
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
873
|
* Produces the vertex list for an orthogonal connector between src and tgt.
|
|
722
874
|
*
|
|
723
875
|
* Link semantics:
|
|
@@ -725,6 +877,11 @@ function routeLinks(links, layouts) {
|
|
|
725
877
|
* SS = source.start → target.start
|
|
726
878
|
* FF = source.finish → target.finish
|
|
727
879
|
* SF = source.start → target.finish
|
|
880
|
+
*
|
|
881
|
+
* @param type - The link type determining start/end anchor points.
|
|
882
|
+
* @param src - The source bar layout.
|
|
883
|
+
* @param tgt - The target bar layout.
|
|
884
|
+
* @returns An ordered array of `Point` vertices.
|
|
728
885
|
*/
|
|
729
886
|
function route(type, src, tgt) {
|
|
730
887
|
let sx, tx;
|
|
@@ -804,8 +961,39 @@ function route(type, src, tgt) {
|
|
|
804
961
|
}
|
|
805
962
|
];
|
|
806
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Computes orthogonal routing for all dependency links.
|
|
966
|
+
* Links whose source or target is not in the layout map are skipped silently
|
|
967
|
+
* (e.g. when the row is collapsed).
|
|
968
|
+
*
|
|
969
|
+
* @param links - The dependency links to route.
|
|
970
|
+
* @param layouts - A map from task ID to its computed {@link BarLayout}.
|
|
971
|
+
* @returns An array of {@link RoutedLink} objects with computed vertex paths.
|
|
972
|
+
*/
|
|
973
|
+
function routeLinks(links, layouts) {
|
|
974
|
+
return links.map((link) => {
|
|
975
|
+
const src = layouts.get(link.source);
|
|
976
|
+
const tgt = layouts.get(link.target);
|
|
977
|
+
if (src === void 0 || tgt === void 0) return null;
|
|
978
|
+
return {
|
|
979
|
+
linkId: link.id,
|
|
980
|
+
sourceTaskId: link.source,
|
|
981
|
+
targetTaskId: link.target,
|
|
982
|
+
points: route(link.type, src, tgt)
|
|
983
|
+
};
|
|
984
|
+
}).filter((r) => r !== null);
|
|
985
|
+
}
|
|
807
986
|
//#endregion
|
|
808
987
|
//#region src/gantt-chart/vanilla/dom/helpers.ts
|
|
988
|
+
/**
|
|
989
|
+
* Batches style assignments; avoids repeated style recalculations.
|
|
990
|
+
*
|
|
991
|
+
* @param elem - The target element.
|
|
992
|
+
* @param styles - A partial CSS style declaration to apply.
|
|
993
|
+
*/
|
|
994
|
+
function css(elem, styles) {
|
|
995
|
+
for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
|
|
996
|
+
}
|
|
809
997
|
function el(tag, props, ns) {
|
|
810
998
|
const elem = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
|
|
811
999
|
if (props !== void 0) for (const [k, v] of Object.entries(props)) if (k === "style" && typeof v === "object" && v !== null) css(elem, v);
|
|
@@ -813,32 +1001,61 @@ function el(tag, props, ns) {
|
|
|
813
1001
|
else elem.setAttribute(k, String(v));
|
|
814
1002
|
return elem;
|
|
815
1003
|
}
|
|
816
|
-
/**
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1004
|
+
/**
|
|
1005
|
+
* Removes all child nodes from elem. Faster than `innerHTML = ''` for large subtrees.
|
|
1006
|
+
*
|
|
1007
|
+
* @param elem - The element to clear.
|
|
1008
|
+
*/
|
|
821
1009
|
function clearChildren(elem) {
|
|
822
1010
|
while (elem.firstChild !== null) elem.removeChild(elem.firstChild);
|
|
823
1011
|
}
|
|
824
|
-
/**
|
|
1012
|
+
/**
|
|
1013
|
+
* Appends all nodes from an array/fragment into parent in one pass.
|
|
1014
|
+
*
|
|
1015
|
+
* @param parent - The parent element.
|
|
1016
|
+
* @param children - The child elements or text nodes to append.
|
|
1017
|
+
*/
|
|
825
1018
|
function appendAll(parent, children) {
|
|
826
1019
|
const frag = document.createDocumentFragment();
|
|
827
1020
|
for (const c of children) frag.appendChild(c);
|
|
828
1021
|
parent.append(frag);
|
|
829
1022
|
}
|
|
830
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* Sets multiple SVG attributes in one call.
|
|
1025
|
+
*
|
|
1026
|
+
* @param elem - The target SVG element.
|
|
1027
|
+
* @param attrs - Attributes to set (values are stringified).
|
|
1028
|
+
*/
|
|
831
1029
|
function setAttrs(elem, attrs) {
|
|
832
1030
|
for (const [k, v] of Object.entries(attrs)) elem.setAttribute(k, String(v));
|
|
833
1031
|
}
|
|
834
1032
|
//#endregion
|
|
835
1033
|
//#region src/gantt-chart/vanilla/dom/timeHeader.ts
|
|
1034
|
+
function specialDayKind(date, specialDaysByDate, showWeekends, weekendDays) {
|
|
1035
|
+
const dateKey = startOfDay(date).toISOString().slice(0, 10);
|
|
1036
|
+
const specialDay = specialDaysByDate.get(dateKey);
|
|
1037
|
+
if (specialDay !== void 0) return specialDay.kind;
|
|
1038
|
+
if (showWeekends && weekendDays.has(date.getUTCDay())) return "weekend";
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Inline style helper local to this module.
|
|
1043
|
+
*
|
|
1044
|
+
* @param elem - The target element.
|
|
1045
|
+
* @param styles - A partial CSS style declaration to apply.
|
|
1046
|
+
*/
|
|
1047
|
+
function css_(elem, styles) {
|
|
1048
|
+
for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
|
|
1049
|
+
}
|
|
836
1050
|
/**
|
|
837
1051
|
* Fully replaces the content of `container` with two header rows.
|
|
838
1052
|
* Called on scale change or viewport change only — not on scroll.
|
|
1053
|
+
*
|
|
1054
|
+
* @param container - The header container element to render into.
|
|
1055
|
+
* @param state - The current chart state.
|
|
839
1056
|
*/
|
|
840
1057
|
function renderTimeHeader(container, state) {
|
|
841
|
-
const { scale, viewportStart, viewportEnd, mapper, totalWidth, locale } = state;
|
|
1058
|
+
const { scale, viewportStart, viewportEnd, mapper, totalWidth, locale, showWeekends, weekendDays, specialDaysByDate } = state;
|
|
842
1059
|
const weekStartsOn = locale.weekStartsOn ?? 1;
|
|
843
1060
|
const upperCells = [];
|
|
844
1061
|
const lowerCells = [];
|
|
@@ -853,7 +1070,8 @@ function renderTimeHeader(container, state) {
|
|
|
853
1070
|
lowerCells.push({
|
|
854
1071
|
label: formatHeaderLabel(cur, scale, locale),
|
|
855
1072
|
x,
|
|
856
|
-
width: w
|
|
1073
|
+
width: w,
|
|
1074
|
+
date: new Date(cur)
|
|
857
1075
|
});
|
|
858
1076
|
const uLabel = formatUpperLabel(cur, scale, locale);
|
|
859
1077
|
if (uLabel !== prevUpperLabel) {
|
|
@@ -928,6 +1146,19 @@ function renderTimeHeader(container, state) {
|
|
|
928
1146
|
whiteSpace: "nowrap"
|
|
929
1147
|
});
|
|
930
1148
|
d.textContent = cell.label;
|
|
1149
|
+
if (scale === "day") {
|
|
1150
|
+
const kind = specialDayKind(cell.date, specialDaysByDate, showWeekends, weekendDays);
|
|
1151
|
+
if (kind !== null) {
|
|
1152
|
+
d.classList.add(`gantt-header-cell--${kind}`);
|
|
1153
|
+
const dateKey = startOfDay(cell.date).toISOString().slice(0, 10);
|
|
1154
|
+
d.dataset["date"] = dateKey;
|
|
1155
|
+
const specialDay = specialDaysByDate.get(dateKey);
|
|
1156
|
+
if (specialDay?.label !== void 0) {
|
|
1157
|
+
d.dataset["label"] = specialDay.label;
|
|
1158
|
+
d.title = specialDay.label;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
931
1162
|
return d;
|
|
932
1163
|
});
|
|
933
1164
|
appendAll(upperRow, upperNodes);
|
|
@@ -936,10 +1167,6 @@ function renderTimeHeader(container, state) {
|
|
|
936
1167
|
container.append(upperRow);
|
|
937
1168
|
container.append(lowerRow);
|
|
938
1169
|
}
|
|
939
|
-
/** Inline style helper local to this module. */
|
|
940
|
-
function css_(elem, styles) {
|
|
941
|
-
for (const [k, v] of Object.entries(styles)) elem.style[k] = v ?? "";
|
|
942
|
-
}
|
|
943
1170
|
//#endregion
|
|
944
1171
|
//#region src/gantt-chart/vanilla/dom/gridColumns.ts
|
|
945
1172
|
const DEFAULT_GRID_COLUMNS = [
|
|
@@ -949,17 +1176,17 @@ const DEFAULT_GRID_COLUMNS = [
|
|
|
949
1176
|
width: "1fr"
|
|
950
1177
|
},
|
|
951
1178
|
{
|
|
952
|
-
id: "
|
|
1179
|
+
id: "startDate",
|
|
953
1180
|
header: "Start time",
|
|
954
1181
|
width: "90px",
|
|
955
|
-
field: "
|
|
1182
|
+
field: "startDate",
|
|
956
1183
|
format: (value, _task, _row, locale) => formatDisplayDate(String(value), locale)
|
|
957
1184
|
},
|
|
958
1185
|
{
|
|
959
|
-
id: "
|
|
1186
|
+
id: "durationHours",
|
|
960
1187
|
header: "Duration",
|
|
961
1188
|
width: "68px",
|
|
962
|
-
field: "
|
|
1189
|
+
field: "durationHours",
|
|
963
1190
|
format: (value) => value > 0 ? String(value) : "—"
|
|
964
1191
|
},
|
|
965
1192
|
{
|
|
@@ -970,27 +1197,30 @@ const DEFAULT_GRID_COLUMNS = [
|
|
|
970
1197
|
];
|
|
971
1198
|
/**
|
|
972
1199
|
* Returns a localized default grid column schema.
|
|
973
|
-
* Column headers use locale label overrides with EN_US_LABELS fallback.
|
|
1200
|
+
* Column headers use locale label overrides with `EN_US_LABELS` fallback.
|
|
1201
|
+
*
|
|
1202
|
+
* @param locale - The {@link ChartLocale} to derive column header labels from.
|
|
1203
|
+
* @returns An array of {@link GridColumn} objects.
|
|
974
1204
|
*/
|
|
975
1205
|
function gridColumnDefaults(locale) {
|
|
976
1206
|
return [
|
|
977
1207
|
{
|
|
978
1208
|
id: "name",
|
|
979
|
-
header: locale.labels?.
|
|
1209
|
+
header: locale.labels?.columnTaskName ?? EN_US_LABELS.columnTaskName,
|
|
980
1210
|
width: "1fr"
|
|
981
1211
|
},
|
|
982
1212
|
{
|
|
983
|
-
id: "
|
|
984
|
-
header: locale.labels?.
|
|
1213
|
+
id: "startDate",
|
|
1214
|
+
header: locale.labels?.columnStartDate ?? EN_US_LABELS.columnStartDate,
|
|
985
1215
|
width: "90px",
|
|
986
|
-
field: "
|
|
1216
|
+
field: "startDate",
|
|
987
1217
|
format: (value, _task, _row, loc) => formatDisplayDate(String(value), loc)
|
|
988
1218
|
},
|
|
989
1219
|
{
|
|
990
|
-
id: "
|
|
991
|
-
header: locale.labels?.
|
|
1220
|
+
id: "durationHours",
|
|
1221
|
+
header: locale.labels?.columnDuration ?? EN_US_LABELS.columnDuration,
|
|
992
1222
|
width: "68px",
|
|
993
|
-
field: "
|
|
1223
|
+
field: "durationHours",
|
|
994
1224
|
format: (value) => value > 0 ? String(value) : "—"
|
|
995
1225
|
},
|
|
996
1226
|
{
|
|
@@ -1000,9 +1230,21 @@ function gridColumnDefaults(locale) {
|
|
|
1000
1230
|
}
|
|
1001
1231
|
];
|
|
1002
1232
|
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Builds a CSS `grid-template-columns` value from a column schema.
|
|
1235
|
+
*
|
|
1236
|
+
* @param columns - The full column schema array (only visible columns are included).
|
|
1237
|
+
* @returns A space-separated CSS track list.
|
|
1238
|
+
*/
|
|
1003
1239
|
function gridTemplateColumns(columns) {
|
|
1004
1240
|
return columns.filter((c) => c.visible !== false).map((c) => c.width).join(" ");
|
|
1005
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Filters a column schema to only visible columns.
|
|
1244
|
+
*
|
|
1245
|
+
* @param columns - The full column schema array.
|
|
1246
|
+
* @returns A new array containing only columns where `visible` is not `false`.
|
|
1247
|
+
*/
|
|
1006
1248
|
function visibleColumns(columns) {
|
|
1007
1249
|
return columns.filter((c) => c.visible !== false);
|
|
1008
1250
|
}
|
|
@@ -1017,6 +1259,13 @@ function parseColumnMinWidth(width) {
|
|
|
1017
1259
|
if (frMatch) return parseFloat(frMatch[1] ?? "0") * 120;
|
|
1018
1260
|
return 0;
|
|
1019
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Computes the minimum natural pixel width of a grid column schema.
|
|
1264
|
+
*
|
|
1265
|
+
* @param columns - The full column schema array.
|
|
1266
|
+
* @returns The sum of minimum widths: `px` columns sum directly, `fr` units contribute
|
|
1267
|
+
* `GRID_COLUMN_FR_MIN_WIDTH` px each.
|
|
1268
|
+
*/
|
|
1020
1269
|
function gridNaturalWidth(columns) {
|
|
1021
1270
|
let total = 0;
|
|
1022
1271
|
for (const col of visibleColumns(columns)) total += parseColumnMinWidth(col.width);
|
|
@@ -1026,65 +1275,18 @@ function gridNaturalWidth(columns) {
|
|
|
1026
1275
|
//#region src/gantt-chart/vanilla/dom/leftPane.ts
|
|
1027
1276
|
const INDENT = 16;
|
|
1028
1277
|
const COLUMN_MIN_WIDTH = 30;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
spacer.style.height = `${paddingBottom}px`;
|
|
1042
|
-
frag.append(spacer);
|
|
1043
|
-
}
|
|
1044
|
-
clearChildren(container);
|
|
1045
|
-
container.append(frag);
|
|
1046
|
-
}
|
|
1047
|
-
function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
|
|
1048
|
-
const selected = row.id === selectedId;
|
|
1049
|
-
const wrapper = el("div");
|
|
1050
|
-
wrapper.className = "gantt-row";
|
|
1051
|
-
css(wrapper, {
|
|
1052
|
-
display: "grid",
|
|
1053
|
-
gridTemplateColumns: gridTemplateColumns(columns),
|
|
1054
|
-
height: `${ROW_HEIGHT}px`,
|
|
1055
|
-
alignItems: "center",
|
|
1056
|
-
paddingLeft: "8px",
|
|
1057
|
-
background: selected ? "var(--gantt-row-selected)" : "var(--gantt-bg)",
|
|
1058
|
-
borderBottom: "1px solid var(--gantt-border)",
|
|
1059
|
-
cursor: "default",
|
|
1060
|
-
boxSizing: "border-box"
|
|
1061
|
-
});
|
|
1062
|
-
wrapper.tabIndex = 0;
|
|
1063
|
-
wrapper.setAttribute("role", "row");
|
|
1064
|
-
wrapper.setAttribute("aria-selected", String(selected));
|
|
1065
|
-
wrapper.dataset["taskId"] = String(row.id);
|
|
1066
|
-
wrapper.addEventListener("click", () => {
|
|
1067
|
-
const task = toTask$1(row);
|
|
1068
|
-
cbs.onRowClick({
|
|
1069
|
-
id: row.id,
|
|
1070
|
-
task
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
wrapper.addEventListener("keydown", (event) => {
|
|
1074
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
1075
|
-
event.preventDefault();
|
|
1076
|
-
cbs.onSelect(row.id);
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
|
|
1080
|
-
return wrapper;
|
|
1081
|
-
}
|
|
1082
|
-
function buildCell(column, row, expandedIds, cbs, locale) {
|
|
1083
|
-
switch (column.id) {
|
|
1084
|
-
case "name": return buildTreeNameCell(row, expandedIds, cbs);
|
|
1085
|
-
case "actions": return buildAddButton(row, cbs, locale);
|
|
1086
|
-
default: return buildDataCell(row, column, locale);
|
|
1087
|
-
}
|
|
1278
|
+
function toTask$1(row) {
|
|
1279
|
+
return {
|
|
1280
|
+
id: row.id,
|
|
1281
|
+
text: row.text,
|
|
1282
|
+
startDate: row.startDate,
|
|
1283
|
+
durationHours: row.durationHours,
|
|
1284
|
+
percentComplete: row.percentComplete,
|
|
1285
|
+
type: row.type,
|
|
1286
|
+
open: row.open,
|
|
1287
|
+
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1288
|
+
...row.color === void 0 ? {} : { color: row.color }
|
|
1289
|
+
};
|
|
1088
1290
|
}
|
|
1089
1291
|
function buildTreeNameCell(row, expandedIds, cbs) {
|
|
1090
1292
|
const hasChildren = isParent(row);
|
|
@@ -1162,7 +1364,7 @@ function buildAddButton(row, cbs, locale) {
|
|
|
1162
1364
|
const btn = el("button");
|
|
1163
1365
|
btn.className = "gantt-add-btn";
|
|
1164
1366
|
btn.textContent = "+";
|
|
1165
|
-
btn.title = locale.labels?.
|
|
1367
|
+
btn.title = locale.labels?.addSubtaskTitle ?? EN_US_LABELS.addSubtaskTitle;
|
|
1166
1368
|
css(btn, {
|
|
1167
1369
|
background: "none",
|
|
1168
1370
|
border: "none",
|
|
@@ -1177,20 +1379,79 @@ function buildAddButton(row, cbs, locale) {
|
|
|
1177
1379
|
});
|
|
1178
1380
|
return btn;
|
|
1179
1381
|
}
|
|
1180
|
-
function
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1382
|
+
function buildCell(column, row, expandedIds, cbs, locale) {
|
|
1383
|
+
switch (column.id) {
|
|
1384
|
+
case "name": return buildTreeNameCell(row, expandedIds, cbs);
|
|
1385
|
+
case "actions": return buildAddButton(row, cbs, locale);
|
|
1386
|
+
default: return buildDataCell(row, column, locale);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
|
|
1390
|
+
const selected = row.id === selectedId;
|
|
1391
|
+
const wrapper = el("div");
|
|
1392
|
+
wrapper.className = "gantt-row";
|
|
1393
|
+
css(wrapper, {
|
|
1394
|
+
display: "grid",
|
|
1395
|
+
gridTemplateColumns: gridTemplateColumns(columns),
|
|
1396
|
+
height: `${ROW_HEIGHT}px`,
|
|
1397
|
+
alignItems: "center",
|
|
1398
|
+
paddingLeft: "8px",
|
|
1399
|
+
background: selected ? "var(--gantt-row-selected)" : "var(--gantt-bg)",
|
|
1400
|
+
borderBottom: "1px solid var(--gantt-border)",
|
|
1401
|
+
cursor: "default",
|
|
1402
|
+
boxSizing: "border-box"
|
|
1403
|
+
});
|
|
1404
|
+
wrapper.tabIndex = 0;
|
|
1405
|
+
wrapper.setAttribute("role", "row");
|
|
1406
|
+
wrapper.setAttribute("aria-selected", String(selected));
|
|
1407
|
+
wrapper.dataset["taskId"] = String(row.id);
|
|
1408
|
+
wrapper.addEventListener("click", () => {
|
|
1409
|
+
const task = toTask$1(row);
|
|
1410
|
+
cbs.onRowClick({
|
|
1411
|
+
id: row.id,
|
|
1412
|
+
task
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
wrapper.addEventListener("keydown", (event) => {
|
|
1416
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1417
|
+
event.preventDefault();
|
|
1418
|
+
cbs.onSelect(row.id);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
|
|
1422
|
+
return wrapper;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Renders the left grid pane.
|
|
1426
|
+
*
|
|
1427
|
+
* @param container - The left pane body element to render into.
|
|
1428
|
+
* @param state - The current chart state.
|
|
1429
|
+
* @param cbs - The left pane callbacks.
|
|
1430
|
+
* @param columns - The grid column schema.
|
|
1431
|
+
*/
|
|
1432
|
+
function renderLeftPane(container, state, cbs, columns) {
|
|
1433
|
+
const { allRows, selectedId, expandedIds, startIndex, endIndex, paddingTop, paddingBottom, locale } = state;
|
|
1434
|
+
const frag = document.createDocumentFragment();
|
|
1435
|
+
if (paddingTop > 0) {
|
|
1436
|
+
const spacer = el("div");
|
|
1437
|
+
spacer.style.height = `${paddingTop}px`;
|
|
1438
|
+
frag.append(spacer);
|
|
1439
|
+
}
|
|
1440
|
+
for (const row of allRows.slice(startIndex, endIndex + 1)) frag.append(buildRow(row, selectedId, expandedIds, cbs, columns, locale));
|
|
1441
|
+
if (paddingBottom > 0) {
|
|
1442
|
+
const spacer = el("div");
|
|
1443
|
+
spacer.style.height = `${paddingBottom}px`;
|
|
1444
|
+
frag.append(spacer);
|
|
1445
|
+
}
|
|
1446
|
+
clearChildren(container);
|
|
1447
|
+
container.append(frag);
|
|
1192
1448
|
}
|
|
1193
|
-
/**
|
|
1449
|
+
/**
|
|
1450
|
+
* Builds the header row for the left pane.
|
|
1451
|
+
*
|
|
1452
|
+
* @param columns - The grid column schema.
|
|
1453
|
+
* @returns The header DOM element.
|
|
1454
|
+
*/
|
|
1194
1455
|
function buildLeftPaneHeader(columns) {
|
|
1195
1456
|
const header = el("div");
|
|
1196
1457
|
css(header, {
|
|
@@ -1246,8 +1507,13 @@ function buildLeftPaneHeader(columns) {
|
|
|
1246
1507
|
}
|
|
1247
1508
|
/**
|
|
1248
1509
|
* Wires up column resize interactions on header handles.
|
|
1249
|
-
* Must be called after the header is in the DOM (so getBoundingClientRect works).
|
|
1250
|
-
*
|
|
1510
|
+
* Must be called after the header is in the DOM (so `getBoundingClientRect` works).
|
|
1511
|
+
*
|
|
1512
|
+
* @param headerEl - The header element containing resize handles.
|
|
1513
|
+
* @param bodyEl - The body element whose rows share the column widths.
|
|
1514
|
+
* @param columns - The grid column schema (mutated in place on resize end).
|
|
1515
|
+
* @param onChange - Optional callback fired on drag end with updated columns.
|
|
1516
|
+
* @returns A cleanup function that removes all resize listeners.
|
|
1251
1517
|
*/
|
|
1252
1518
|
function setupColumnResize(headerEl, bodyEl, columns, onChange) {
|
|
1253
1519
|
const handles = headerEl.querySelectorAll(".gantt-col-resize-handle");
|
|
@@ -1306,6 +1572,10 @@ const ARROW_SIZE = 6;
|
|
|
1306
1572
|
/**
|
|
1307
1573
|
* Creates the SVG overlay element. Call once; pass to updateDependencyLayer on each render.
|
|
1308
1574
|
* Also creates a hidden ghost-line path used during link-creation drags.
|
|
1575
|
+
*
|
|
1576
|
+
* @param totalWidth - The total pixel width of the SVG viewport.
|
|
1577
|
+
* @param totalHeight - The total pixel height of the SVG viewport.
|
|
1578
|
+
* @returns An `SVGSVGElement` ready to be inserted into the DOM.
|
|
1309
1579
|
*/
|
|
1310
1580
|
function createDependencyLayer(totalWidth, totalHeight) {
|
|
1311
1581
|
const svg = document.createElementNS(NS, "svg");
|
|
@@ -1357,7 +1627,13 @@ function createDependencyLayer(totalWidth, totalHeight) {
|
|
|
1357
1627
|
}
|
|
1358
1628
|
/**
|
|
1359
1629
|
* Shows or updates the ghost line drawn during a link-creation drag.
|
|
1360
|
-
*
|
|
1630
|
+
*
|
|
1631
|
+
* @param svg - The SVG dependency layer element.
|
|
1632
|
+
* @param x1 - Start X coordinate.
|
|
1633
|
+
* @param y1 - Start Y coordinate.
|
|
1634
|
+
* @param x2 - End X coordinate.
|
|
1635
|
+
* @param y2 - End Y coordinate.
|
|
1636
|
+
* @param valid - When `true`, the line is drawn solid with an arrow marker.
|
|
1361
1637
|
*/
|
|
1362
1638
|
function showGhostLine(svg, x1, y1, x2, y2, valid) {
|
|
1363
1639
|
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
@@ -1372,6 +1648,8 @@ function showGhostLine(svg, x1, y1, x2, y2, valid) {
|
|
|
1372
1648
|
}
|
|
1373
1649
|
/**
|
|
1374
1650
|
* Hides the ghost line after a link-creation drag completes or is cancelled.
|
|
1651
|
+
*
|
|
1652
|
+
* @param svg - The SVG dependency layer element.
|
|
1375
1653
|
*/
|
|
1376
1654
|
function hideGhostLine(svg) {
|
|
1377
1655
|
const ghost = svg.querySelector("path.gantt-ghost-line");
|
|
@@ -1383,6 +1661,13 @@ function hideGhostLine(svg) {
|
|
|
1383
1661
|
/**
|
|
1384
1662
|
* Replaces all path elements in the SVG to reflect the current link set.
|
|
1385
1663
|
* The `<defs>` node (first child) is preserved.
|
|
1664
|
+
*
|
|
1665
|
+
* @param svg - The SVG dependency layer element.
|
|
1666
|
+
* @param links - The array of routed links to render.
|
|
1667
|
+
* @param totalWidth - The total pixel width of the SVG viewport.
|
|
1668
|
+
* @param totalHeight - The total pixel height of the SVG viewport.
|
|
1669
|
+
* @param selectedTaskId - The currently selected task ID, or `null`.
|
|
1670
|
+
* @param highlightLinkedDependenciesOnSelect - When `true`, links connected to the selected task use highlight styling.
|
|
1386
1671
|
*/
|
|
1387
1672
|
function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTaskId, highlightLinkedDependenciesOnSelect) {
|
|
1388
1673
|
setAttrs(svg, {
|
|
@@ -1418,12 +1703,31 @@ function updateDependencyLayer(svg, links, totalWidth, totalHeight, selectedTask
|
|
|
1418
1703
|
}
|
|
1419
1704
|
//#endregion
|
|
1420
1705
|
//#region src/gantt-chart/vanilla/interaction/drag.ts
|
|
1706
|
+
function toTask(row) {
|
|
1707
|
+
return {
|
|
1708
|
+
id: row.id,
|
|
1709
|
+
text: row.text,
|
|
1710
|
+
startDate: row.startDate,
|
|
1711
|
+
durationHours: row.durationHours,
|
|
1712
|
+
percentComplete: row.percentComplete,
|
|
1713
|
+
type: row.type,
|
|
1714
|
+
open: row.open,
|
|
1715
|
+
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1716
|
+
...row.color === void 0 ? {} : { color: row.color }
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1421
1719
|
/**
|
|
1422
1720
|
* Attaches drag-to-move and resize listeners to a bar element.
|
|
1423
|
-
* Returns a cleanup function that removes all listeners.
|
|
1424
1721
|
*
|
|
1425
1722
|
* Design: all mutable state lives in closure variables captured at mousedown.
|
|
1426
1723
|
* No global state; multiple bars can be dragged independently (one at a time).
|
|
1724
|
+
*
|
|
1725
|
+
* @param barEl - The bar DOM element.
|
|
1726
|
+
* @param resizeHandleEl - The resize handle DOM element.
|
|
1727
|
+
* @param task - The {@link TaskNode} for this bar.
|
|
1728
|
+
* @param getMapper - A function returning the current {@link PixelMapper} (snapshotted at mousedown).
|
|
1729
|
+
* @param cbs - The chart callbacks.
|
|
1730
|
+
* @returns A cleanup function that removes all listeners.
|
|
1427
1731
|
*/
|
|
1428
1732
|
function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
1429
1733
|
function onBarDown(e) {
|
|
@@ -1434,14 +1738,14 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1434
1738
|
} catch {}
|
|
1435
1739
|
cbs.onSelect?.(task.id);
|
|
1436
1740
|
const startX = e.clientX;
|
|
1437
|
-
const originDate = parseDate(task.
|
|
1741
|
+
const originDate = parseDate(task.startDate);
|
|
1438
1742
|
const mapper = getMapper();
|
|
1439
1743
|
function onMove(me) {
|
|
1440
1744
|
const dx = me.clientX - startX;
|
|
1441
|
-
const
|
|
1745
|
+
const hours = Math.round(mapper.widthToDuration(dx));
|
|
1442
1746
|
cbs.onMove?.({
|
|
1443
1747
|
id: task.id,
|
|
1444
|
-
startDate:
|
|
1748
|
+
startDate: addHours(originDate, hours)
|
|
1445
1749
|
});
|
|
1446
1750
|
}
|
|
1447
1751
|
function onUp() {
|
|
@@ -1461,14 +1765,14 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1461
1765
|
resizeHandleEl.setPointerCapture(e.pointerId);
|
|
1462
1766
|
} catch {}
|
|
1463
1767
|
const startX = e.clientX;
|
|
1464
|
-
const origDur = task.
|
|
1768
|
+
const origDur = task.durationHours;
|
|
1465
1769
|
const mapper = getMapper();
|
|
1466
1770
|
function onMove(me) {
|
|
1467
1771
|
const dx = me.clientX - startX;
|
|
1468
|
-
const
|
|
1772
|
+
const hoursDelta = Math.round(mapper.widthToDuration(dx));
|
|
1469
1773
|
cbs.onResize?.({
|
|
1470
1774
|
id: task.id,
|
|
1471
|
-
|
|
1775
|
+
durationHours: Math.max(1, origDur + hoursDelta)
|
|
1472
1776
|
});
|
|
1473
1777
|
}
|
|
1474
1778
|
function onUp() {
|
|
@@ -1483,7 +1787,7 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1483
1787
|
cbs.onTaskEditIntent?.({
|
|
1484
1788
|
id: task.id,
|
|
1485
1789
|
source: "bar",
|
|
1486
|
-
trigger: "
|
|
1790
|
+
trigger: "doubleClick",
|
|
1487
1791
|
task: toTask(task)
|
|
1488
1792
|
});
|
|
1489
1793
|
}
|
|
@@ -1498,7 +1802,11 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
|
|
|
1498
1802
|
}
|
|
1499
1803
|
/**
|
|
1500
1804
|
* Attaches click-to-select on a milestone diamond.
|
|
1501
|
-
*
|
|
1805
|
+
*
|
|
1806
|
+
* @param diamondEl - The milestone diamond DOM element.
|
|
1807
|
+
* @param taskId - The task ID to select.
|
|
1808
|
+
* @param cbs - The chart callbacks.
|
|
1809
|
+
* @returns A cleanup function that removes all listeners.
|
|
1502
1810
|
*/
|
|
1503
1811
|
function attachMilestoneClick(diamondEl, taskId, cbs) {
|
|
1504
1812
|
function onClick() {
|
|
@@ -1511,7 +1819,7 @@ function attachMilestoneClick(diamondEl, taskId, cbs) {
|
|
|
1511
1819
|
cbs.onTaskEditIntent?.({
|
|
1512
1820
|
id: taskId,
|
|
1513
1821
|
source: "milestone",
|
|
1514
|
-
trigger: "
|
|
1822
|
+
trigger: "doubleClick",
|
|
1515
1823
|
task
|
|
1516
1824
|
});
|
|
1517
1825
|
}
|
|
@@ -1526,24 +1834,19 @@ function attachMilestoneClick(diamondEl, taskId, cbs) {
|
|
|
1526
1834
|
function bindMilestoneTask(diamondEl, task) {
|
|
1527
1835
|
diamondEl.__task = task;
|
|
1528
1836
|
}
|
|
1529
|
-
function toTask(row) {
|
|
1530
|
-
return {
|
|
1531
|
-
id: row.id,
|
|
1532
|
-
text: row.text,
|
|
1533
|
-
start_date: row.start_date,
|
|
1534
|
-
duration: row.duration,
|
|
1535
|
-
progress: row.progress,
|
|
1536
|
-
type: row.type,
|
|
1537
|
-
open: row.open,
|
|
1538
|
-
...row.parent === void 0 ? {} : { parent: row.parent },
|
|
1539
|
-
...row.color === void 0 ? {} : { color: row.color }
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
1837
|
//#endregion
|
|
1543
1838
|
//#region src/gantt-chart/vanilla/interaction/linkCreation.ts
|
|
1544
1839
|
/**
|
|
1545
1840
|
* Attaches a link-creation drag listener to an endpoint handle.
|
|
1546
|
-
*
|
|
1841
|
+
*
|
|
1842
|
+
* @param handle - The endpoint DOM element.
|
|
1843
|
+
* @param sourceTaskId - The task ID from which the link originates.
|
|
1844
|
+
* @param anchorX - The X anchor coordinate (task center).
|
|
1845
|
+
* @param anchorY - The Y anchor coordinate (task center).
|
|
1846
|
+
* @param svgLayer - The SVG dependency layer element for ghost line rendering.
|
|
1847
|
+
* @param absoluteLayer - The absolute-positioned layer for coordinate calculations.
|
|
1848
|
+
* @param cbs - The chart callbacks.
|
|
1849
|
+
* @returns A cleanup function that removes all listeners.
|
|
1547
1850
|
*/
|
|
1548
1851
|
function attachLinkEndpointHandle(handle, sourceTaskId, anchorX, anchorY, svgLayer, absoluteLayer, cbs) {
|
|
1549
1852
|
function onPointerDown(e) {
|
|
@@ -1592,6 +1895,8 @@ function attachLinkEndpointHandle(handle, sourceTaskId, anchorX, anchorY, svgLay
|
|
|
1592
1895
|
/**
|
|
1593
1896
|
* Creates an endpoint handle DOM element.
|
|
1594
1897
|
* The caller must position it with inline styles and append it to the layer.
|
|
1898
|
+
*
|
|
1899
|
+
* @returns A new `HTMLElement` with the `gantt-link-endpoint` class.
|
|
1595
1900
|
*/
|
|
1596
1901
|
function createEndpointHandle() {
|
|
1597
1902
|
const handle = document.createElement("div");
|
|
@@ -1618,7 +1923,11 @@ const BAR_COLOR = {
|
|
|
1618
1923
|
project: "var(--gantt-project)",
|
|
1619
1924
|
milestone: "var(--gantt-milestone)"
|
|
1620
1925
|
};
|
|
1621
|
-
/**
|
|
1926
|
+
/**
|
|
1927
|
+
* Creates the skeleton DOM structure for the right pane. Call once.
|
|
1928
|
+
*
|
|
1929
|
+
* @returns A new {@link RightPaneRefs} with empty containers and bar registry.
|
|
1930
|
+
*/
|
|
1622
1931
|
function createRightPaneRefs() {
|
|
1623
1932
|
const scrollContainer = el("div");
|
|
1624
1933
|
const stripeContainer = el("div");
|
|
@@ -1627,7 +1936,7 @@ function createRightPaneRefs() {
|
|
|
1627
1936
|
css(stripeContainer, { position: "relative" });
|
|
1628
1937
|
css(absoluteLayer, {
|
|
1629
1938
|
position: "absolute",
|
|
1630
|
-
top: "
|
|
1939
|
+
top: "52px",
|
|
1631
1940
|
left: "0"
|
|
1632
1941
|
});
|
|
1633
1942
|
scrollContainer.append(stripeContainer);
|
|
@@ -1641,94 +1950,8 @@ function createRightPaneRefs() {
|
|
|
1641
1950
|
barRegistry: /* @__PURE__ */ new Map()
|
|
1642
1951
|
};
|
|
1643
1952
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
* Grid lines and stripes are rebuilt each call (cheap — no event listeners).
|
|
1647
|
-
* Bars are rebuilt each call with fresh drag listeners (old ones cleaned up first).
|
|
1648
|
-
*/
|
|
1649
|
-
function renderRightPane(refs, state, cbs) {
|
|
1650
|
-
const { allRows, layouts, links, mapper, scale, viewportStart, viewportEnd, totalWidth, selectedId, highlightLinkedDependenciesOnSelect, paddingTop, paddingBottom, startIndex } = state;
|
|
1651
|
-
const { stripeContainer, absoluteLayer, svgLayer, barRegistry } = refs;
|
|
1652
|
-
const rowCount = allRows.length;
|
|
1653
|
-
const contentHeight = totalContentHeight(rowCount);
|
|
1654
|
-
const visibleRows = allRows.slice(state.startIndex, state.endIndex + 1);
|
|
1655
|
-
clearChildren(stripeContainer);
|
|
1656
|
-
css(stripeContainer, { width: `${totalWidth}px` });
|
|
1657
|
-
if (paddingTop > 0) {
|
|
1658
|
-
const s = el("div");
|
|
1659
|
-
s.style.height = `${paddingTop}px`;
|
|
1660
|
-
stripeContainer.append(s);
|
|
1661
|
-
}
|
|
1662
|
-
for (let i = 0; i < visibleRows.length; i++) {
|
|
1663
|
-
const rowIdx = startIndex + i;
|
|
1664
|
-
const stripe = el("div");
|
|
1665
|
-
css(stripe, {
|
|
1666
|
-
height: `${ROW_HEIGHT}px`,
|
|
1667
|
-
background: rowIdx % 2 === 0 ? "var(--gantt-bg)" : "var(--gantt-stripe)",
|
|
1668
|
-
borderBottom: "1px solid var(--gantt-border)"
|
|
1669
|
-
});
|
|
1670
|
-
stripeContainer.append(stripe);
|
|
1671
|
-
}
|
|
1672
|
-
if (paddingBottom > 0) {
|
|
1673
|
-
const s = el("div");
|
|
1674
|
-
s.style.height = `${paddingBottom}px`;
|
|
1675
|
-
stripeContainer.append(s);
|
|
1676
|
-
}
|
|
1677
|
-
css(absoluteLayer, {
|
|
1678
|
-
width: `${totalWidth}px`,
|
|
1679
|
-
height: `${contentHeight}px`
|
|
1680
|
-
});
|
|
1681
|
-
const toRemove = [];
|
|
1682
|
-
for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
|
|
1683
|
-
for (const node of toRemove) absoluteLayer.removeChild(node);
|
|
1684
|
-
hideGhostLine(svgLayer);
|
|
1685
|
-
for (const { cleanupDrag, cleanupLinkHandles } of barRegistry.values()) {
|
|
1686
|
-
cleanupDrag();
|
|
1687
|
-
cleanupLinkHandles?.();
|
|
1688
|
-
}
|
|
1689
|
-
barRegistry.clear();
|
|
1690
|
-
if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
|
|
1691
|
-
let gridCur = snapToScaleBoundary(viewportStart, scale);
|
|
1692
|
-
while (gridCur <= viewportEnd) {
|
|
1693
|
-
const x = mapper.toX(gridCur);
|
|
1694
|
-
const line = el("div");
|
|
1695
|
-
css(line, {
|
|
1696
|
-
position: "absolute",
|
|
1697
|
-
left: `${x}px`,
|
|
1698
|
-
top: "0",
|
|
1699
|
-
width: "1px",
|
|
1700
|
-
height: `${contentHeight}px`,
|
|
1701
|
-
background: "var(--gantt-grid-line)",
|
|
1702
|
-
pointerEvents: "none"
|
|
1703
|
-
});
|
|
1704
|
-
absoluteLayer.insertBefore(line, svgLayer);
|
|
1705
|
-
gridCur = nextScaleBoundary(gridCur, scale);
|
|
1706
|
-
}
|
|
1707
|
-
const todayX = mapper.toX(/* @__PURE__ */ new Date());
|
|
1708
|
-
const todayLineWidth = 2;
|
|
1709
|
-
if (todayX >= 0 && todayX <= totalWidth - todayLineWidth) {
|
|
1710
|
-
const todayLine = el("div");
|
|
1711
|
-
todayLine.className = "gantt-today-marker";
|
|
1712
|
-
css(todayLine, {
|
|
1713
|
-
position: "absolute",
|
|
1714
|
-
left: `${todayX}px`,
|
|
1715
|
-
top: "0",
|
|
1716
|
-
width: `${todayLineWidth}px`,
|
|
1717
|
-
height: `${contentHeight}px`,
|
|
1718
|
-
background: "var(--gantt-today)",
|
|
1719
|
-
pointerEvents: "none",
|
|
1720
|
-
zIndex: "5"
|
|
1721
|
-
});
|
|
1722
|
-
absoluteLayer.insertBefore(todayLine, svgLayer);
|
|
1723
|
-
}
|
|
1724
|
-
const visibleTaskIds = new Set(visibleRows.map((task) => task.id));
|
|
1725
|
-
for (const task of visibleRows) {
|
|
1726
|
-
const layout = layouts.get(task.id);
|
|
1727
|
-
if (layout === void 0) continue;
|
|
1728
|
-
if (layout.type === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
|
|
1729
|
-
else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
|
|
1730
|
-
}
|
|
1731
|
-
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect);
|
|
1953
|
+
function ariaLabel(locale, key, arg) {
|
|
1954
|
+
return formatLabel(locale.labels?.[key] ?? EN_US_LABELS[key], arg);
|
|
1732
1955
|
}
|
|
1733
1956
|
function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
|
|
1734
1957
|
const { mapper, viewportStart, viewportEnd, showWeekends, weekendDays, specialDaysByDate } = state;
|
|
@@ -1820,7 +2043,7 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
|
|
|
1820
2043
|
bar.append(label);
|
|
1821
2044
|
bar.tabIndex = 0;
|
|
1822
2045
|
bar.setAttribute("role", "button");
|
|
1823
|
-
bar.setAttribute("aria-label", ariaLabel(state.locale, "
|
|
2046
|
+
bar.setAttribute("aria-label", ariaLabel(state.locale, "ariaTask", task.text));
|
|
1824
2047
|
bar.setAttribute("aria-pressed", String(selected));
|
|
1825
2048
|
bar.dataset["taskId"] = String(task.id);
|
|
1826
2049
|
bar.addEventListener("click", () => {
|
|
@@ -1907,7 +2130,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
1907
2130
|
});
|
|
1908
2131
|
diamond.tabIndex = 0;
|
|
1909
2132
|
diamond.setAttribute("role", "button");
|
|
1910
|
-
diamond.setAttribute("aria-label", ariaLabel(state.locale, "
|
|
2133
|
+
diamond.setAttribute("aria-label", ariaLabel(state.locale, "ariaMilestone", task.text));
|
|
1911
2134
|
diamond.setAttribute("aria-pressed", String(selected));
|
|
1912
2135
|
diamond.dataset["taskId"] = String(task.id);
|
|
1913
2136
|
diamond.addEventListener("keydown", (event) => {
|
|
@@ -1967,8 +2190,98 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
|
|
|
1967
2190
|
if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
|
|
1968
2191
|
registry.set(task.id, entry);
|
|
1969
2192
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
2193
|
+
/**
|
|
2194
|
+
* Full render of the right pane.
|
|
2195
|
+
* Grid lines and stripes are rebuilt each call (cheap — no event listeners).
|
|
2196
|
+
* Bars are rebuilt each call with fresh drag listeners (old ones cleaned up first).
|
|
2197
|
+
*
|
|
2198
|
+
* @param refs - The right pane DOM references.
|
|
2199
|
+
* @param state - The current chart state.
|
|
2200
|
+
* @param cbs - The chart callbacks.
|
|
2201
|
+
*/
|
|
2202
|
+
function renderRightPane(refs, state, cbs) {
|
|
2203
|
+
const { allRows, layouts, links, mapper, scale, viewportStart, viewportEnd, totalWidth, selectedId, highlightLinkedDependenciesOnSelect, paddingTop, paddingBottom, startIndex } = state;
|
|
2204
|
+
const { stripeContainer, absoluteLayer, svgLayer, barRegistry } = refs;
|
|
2205
|
+
const rowCount = allRows.length;
|
|
2206
|
+
const contentHeight = totalContentHeight(rowCount);
|
|
2207
|
+
const visibleRows = allRows.slice(state.startIndex, state.endIndex + 1);
|
|
2208
|
+
clearChildren(stripeContainer);
|
|
2209
|
+
css(stripeContainer, { width: `${totalWidth}px` });
|
|
2210
|
+
if (paddingTop > 0) {
|
|
2211
|
+
const s = el("div");
|
|
2212
|
+
s.style.height = `${paddingTop}px`;
|
|
2213
|
+
stripeContainer.append(s);
|
|
2214
|
+
}
|
|
2215
|
+
for (let i = 0; i < visibleRows.length; i++) {
|
|
2216
|
+
const rowIdx = startIndex + i;
|
|
2217
|
+
const stripe = el("div");
|
|
2218
|
+
css(stripe, {
|
|
2219
|
+
height: `${ROW_HEIGHT}px`,
|
|
2220
|
+
background: rowIdx % 2 === 0 ? "var(--gantt-bg)" : "var(--gantt-stripe)",
|
|
2221
|
+
borderBottom: "1px solid var(--gantt-border)"
|
|
2222
|
+
});
|
|
2223
|
+
stripeContainer.append(stripe);
|
|
2224
|
+
}
|
|
2225
|
+
if (paddingBottom > 0) {
|
|
2226
|
+
const s = el("div");
|
|
2227
|
+
s.style.height = `${paddingBottom}px`;
|
|
2228
|
+
stripeContainer.append(s);
|
|
2229
|
+
}
|
|
2230
|
+
css(absoluteLayer, {
|
|
2231
|
+
width: `${totalWidth}px`,
|
|
2232
|
+
height: `${contentHeight}px`
|
|
2233
|
+
});
|
|
2234
|
+
const toRemove = [];
|
|
2235
|
+
for (const child of [...absoluteLayer.children]) if (child !== svgLayer) toRemove.push(child);
|
|
2236
|
+
for (const node of toRemove) absoluteLayer.removeChild(node);
|
|
2237
|
+
hideGhostLine(svgLayer);
|
|
2238
|
+
for (const { cleanupDrag, cleanupLinkHandles } of barRegistry.values()) {
|
|
2239
|
+
cleanupDrag();
|
|
2240
|
+
cleanupLinkHandles?.();
|
|
2241
|
+
}
|
|
2242
|
+
barRegistry.clear();
|
|
2243
|
+
if (scale === "day") renderSpecialDayBackgrounds(absoluteLayer, svgLayer, state, contentHeight);
|
|
2244
|
+
let gridCur = snapToScaleBoundary(viewportStart, scale);
|
|
2245
|
+
while (gridCur <= viewportEnd) {
|
|
2246
|
+
const x = mapper.toX(gridCur);
|
|
2247
|
+
const line = el("div");
|
|
2248
|
+
css(line, {
|
|
2249
|
+
position: "absolute",
|
|
2250
|
+
left: `${x}px`,
|
|
2251
|
+
top: "0",
|
|
2252
|
+
width: "1px",
|
|
2253
|
+
height: `${contentHeight}px`,
|
|
2254
|
+
background: "var(--gantt-grid-line)",
|
|
2255
|
+
pointerEvents: "none"
|
|
2256
|
+
});
|
|
2257
|
+
absoluteLayer.insertBefore(line, svgLayer);
|
|
2258
|
+
gridCur = nextScaleBoundary(gridCur, scale);
|
|
2259
|
+
}
|
|
2260
|
+
const todayX = mapper.toX(/* @__PURE__ */ new Date());
|
|
2261
|
+
const todayLineWidth = 2;
|
|
2262
|
+
if (todayX >= 0 && todayX <= totalWidth - todayLineWidth) {
|
|
2263
|
+
const todayLine = el("div");
|
|
2264
|
+
todayLine.className = "gantt-today-marker";
|
|
2265
|
+
css(todayLine, {
|
|
2266
|
+
position: "absolute",
|
|
2267
|
+
left: `${todayX}px`,
|
|
2268
|
+
top: "0",
|
|
2269
|
+
width: `${todayLineWidth}px`,
|
|
2270
|
+
height: `${contentHeight}px`,
|
|
2271
|
+
background: "var(--gantt-today)",
|
|
2272
|
+
pointerEvents: "none",
|
|
2273
|
+
zIndex: "5"
|
|
2274
|
+
});
|
|
2275
|
+
absoluteLayer.insertBefore(todayLine, svgLayer);
|
|
2276
|
+
}
|
|
2277
|
+
const visibleTaskIds = new Set(visibleRows.map((task) => task.id));
|
|
2278
|
+
for (const task of visibleRows) {
|
|
2279
|
+
const layout = layouts.get(task.id);
|
|
2280
|
+
if (layout === void 0) continue;
|
|
2281
|
+
if (layout.type === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
|
|
2282
|
+
else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
|
|
2283
|
+
}
|
|
2284
|
+
updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect);
|
|
1972
2285
|
}
|
|
1973
2286
|
//#endregion
|
|
1974
2287
|
//#region src/gantt-chart/vanilla/utils.ts
|
|
@@ -1980,6 +2293,9 @@ function buildTaskIndex(tasks) {
|
|
|
1980
2293
|
}
|
|
1981
2294
|
return index;
|
|
1982
2295
|
}
|
|
2296
|
+
function toIsoDate(date) {
|
|
2297
|
+
return date.toISOString().slice(0, 10);
|
|
2298
|
+
}
|
|
1983
2299
|
function buildSpecialDayIndex(specialDays) {
|
|
1984
2300
|
const map = /* @__PURE__ */ new Map();
|
|
1985
2301
|
for (const specialDay of specialDays) {
|
|
@@ -1993,9 +2309,6 @@ function buildSpecialDayIndex(specialDays) {
|
|
|
1993
2309
|
}
|
|
1994
2310
|
return map;
|
|
1995
2311
|
}
|
|
1996
|
-
function toIsoDate(date) {
|
|
1997
|
-
return date.toISOString().slice(0, 10);
|
|
1998
|
-
}
|
|
1999
2312
|
function normalizeWeekendDays(days) {
|
|
2000
2313
|
if (days === void 0) return new Set([0, 6]);
|
|
2001
2314
|
const normalized = /* @__PURE__ */ new Set();
|
|
@@ -2076,10 +2389,23 @@ function computeLeftPaneWidth(options) {
|
|
|
2076
2389
|
//#region src/gantt-chart/vanilla/gantt-chart.ts
|
|
2077
2390
|
const HEADER_H = 52;
|
|
2078
2391
|
const OVERSCAN = 4;
|
|
2392
|
+
/**
|
|
2393
|
+
* Progressive-enhancement Gantt chart component.
|
|
2394
|
+
* Validates input, builds a DOM tree, and renders a full interactive chart
|
|
2395
|
+
* inside the given container element.
|
|
2396
|
+
*
|
|
2397
|
+
* @example
|
|
2398
|
+
* ```ts
|
|
2399
|
+
* const chart = new GanttChart(document.getElementById('chart')!, input, {
|
|
2400
|
+
* locale: 'de-DE',
|
|
2401
|
+
* theme: 'dark',
|
|
2402
|
+
* });
|
|
2403
|
+
* ```
|
|
2404
|
+
*/
|
|
2079
2405
|
var GanttChart = class {
|
|
2080
2406
|
#container;
|
|
2081
2407
|
#opts;
|
|
2082
|
-
#input;
|
|
2408
|
+
#input = null;
|
|
2083
2409
|
#scale;
|
|
2084
2410
|
#selectedId = null;
|
|
2085
2411
|
#scrollTop = 0;
|
|
@@ -2107,14 +2433,18 @@ var GanttChart = class {
|
|
|
2107
2433
|
#cbs;
|
|
2108
2434
|
#resizeObserver = null;
|
|
2109
2435
|
#columnResizeCleanup;
|
|
2110
|
-
|
|
2436
|
+
/**
|
|
2437
|
+
* Constructs a new chart, builds the DOM, and wires internal event handling.
|
|
2438
|
+
* Data must be loaded via {@link update} before the chart renders.
|
|
2439
|
+
*
|
|
2440
|
+
* @param container - The host `HTMLElement` the chart will be appended to.
|
|
2441
|
+
* @param opts - Configuration and callback options.
|
|
2442
|
+
*/
|
|
2443
|
+
constructor(container, opts = {}) {
|
|
2111
2444
|
this.#container = container;
|
|
2112
|
-
validateLinkRefs(input.tasks, input.links);
|
|
2113
|
-
detectCycles(input.tasks, input.links);
|
|
2114
|
-
this.#input = input;
|
|
2115
2445
|
this.#scale = opts.scale ?? "day";
|
|
2116
2446
|
this.#opts = opts;
|
|
2117
|
-
this.#taskIndex =
|
|
2447
|
+
this.#taskIndex = /* @__PURE__ */ new Map();
|
|
2118
2448
|
this.#locale = resolveChartLocale(opts.locale);
|
|
2119
2449
|
this.#columns = opts.gridColumns ?? gridColumnDefaults(this.#locale);
|
|
2120
2450
|
this.#leftPaneDefaultWidth = opts.leftPaneWidth ?? gridNaturalWidth(this.#columns);
|
|
@@ -2122,7 +2452,7 @@ var GanttChart = class {
|
|
|
2122
2452
|
this.#timelineMinWidth = opts.timelineMinWidth ?? 220;
|
|
2123
2453
|
this.#weekendDays = normalizeWeekendDays(opts.weekendDays ?? this.#locale.weekendDays);
|
|
2124
2454
|
this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays ?? []);
|
|
2125
|
-
this.#expandedIds =
|
|
2455
|
+
this.#expandedIds = /* @__PURE__ */ new Set();
|
|
2126
2456
|
this.#cbs = {
|
|
2127
2457
|
onSelect: (id) => {
|
|
2128
2458
|
if (this.#selectedId === id) return;
|
|
@@ -2142,12 +2472,12 @@ var GanttChart = class {
|
|
|
2142
2472
|
},
|
|
2143
2473
|
onMove: (payload) => {
|
|
2144
2474
|
const iso = payload.startDate.toISOString().slice(0, 10);
|
|
2145
|
-
this.#patchTask(payload.id, {
|
|
2475
|
+
this.#patchTask(payload.id, { startDate: iso });
|
|
2146
2476
|
opts.onMove?.(payload);
|
|
2147
2477
|
this.#scheduleRender();
|
|
2148
2478
|
},
|
|
2149
2479
|
onResize: (payload) => {
|
|
2150
|
-
this.#patchTask(payload.id, {
|
|
2480
|
+
this.#patchTask(payload.id, { durationHours: payload.durationHours });
|
|
2151
2481
|
opts.onResize?.(payload);
|
|
2152
2482
|
this.#scheduleRender();
|
|
2153
2483
|
},
|
|
@@ -2167,21 +2497,44 @@ var GanttChart = class {
|
|
|
2167
2497
|
this.#applyTheme();
|
|
2168
2498
|
this.#applyResponsivePaneStyles();
|
|
2169
2499
|
this.#setupResizeObserver();
|
|
2170
|
-
this.#render();
|
|
2171
2500
|
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Replaces the full dataset and re-renders.
|
|
2503
|
+
*
|
|
2504
|
+
* @param newInput - The new {@link GanttInput} to apply.
|
|
2505
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
2506
|
+
*/
|
|
2172
2507
|
update(newInput) {
|
|
2173
2508
|
this.#assertAlive();
|
|
2174
2509
|
validateLinkRefs(newInput.tasks, newInput.links);
|
|
2175
2510
|
detectCycles(newInput.tasks, newInput.links);
|
|
2176
2511
|
this.#input = newInput;
|
|
2177
2512
|
this.#taskIndex = buildTaskIndex(newInput.tasks);
|
|
2178
|
-
this.#
|
|
2513
|
+
this.#expandedIds = getInitialExpandedIds(newInput.tasks);
|
|
2514
|
+
if (this.#rafPending && this.#rafId !== null) {
|
|
2515
|
+
cancelAnimationFrame(this.#rafId);
|
|
2516
|
+
this.#rafId = null;
|
|
2517
|
+
this.#rafPending = false;
|
|
2518
|
+
}
|
|
2519
|
+
this.#render();
|
|
2179
2520
|
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Switches the time scale and re-renders.
|
|
2523
|
+
*
|
|
2524
|
+
* @param scale - The new {@link TimeScale} to display.
|
|
2525
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
2526
|
+
*/
|
|
2180
2527
|
setScale(scale) {
|
|
2181
2528
|
this.#assertAlive();
|
|
2182
2529
|
this.#scale = scale;
|
|
2183
2530
|
this.#scheduleRender();
|
|
2184
2531
|
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Programmatically selects or deselects a task.
|
|
2534
|
+
*
|
|
2535
|
+
* @param id - The task ID to select, or `null` to clear the selection.
|
|
2536
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
2537
|
+
*/
|
|
2185
2538
|
select(id) {
|
|
2186
2539
|
this.#assertAlive();
|
|
2187
2540
|
this.#selectedId = id;
|
|
@@ -2193,6 +2546,11 @@ var GanttChart = class {
|
|
|
2193
2546
|
}
|
|
2194
2547
|
this.#render();
|
|
2195
2548
|
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Collapses all expandable groups in the task tree.
|
|
2551
|
+
*
|
|
2552
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
2553
|
+
*/
|
|
2196
2554
|
collapseAll() {
|
|
2197
2555
|
this.#assertAlive();
|
|
2198
2556
|
this.#expandedIds.clear();
|
|
@@ -2203,10 +2561,15 @@ var GanttChart = class {
|
|
|
2203
2561
|
}
|
|
2204
2562
|
this.#render();
|
|
2205
2563
|
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Expands all expandable groups in the task tree.
|
|
2566
|
+
*
|
|
2567
|
+
* @throws {GanttError} When the instance has been destroyed.
|
|
2568
|
+
*/
|
|
2206
2569
|
expandAll() {
|
|
2207
2570
|
this.#assertAlive();
|
|
2208
2571
|
this.#expandedIds.clear();
|
|
2209
|
-
for (const id of getExpandableTaskIds(this.#input.tasks)) this.#expandedIds.add(id);
|
|
2572
|
+
if (this.#input !== null) for (const id of getExpandableTaskIds(this.#input.tasks)) this.#expandedIds.add(id);
|
|
2210
2573
|
if (this.#rafPending && this.#rafId !== null) {
|
|
2211
2574
|
cancelAnimationFrame(this.#rafId);
|
|
2212
2575
|
this.#rafId = null;
|
|
@@ -2214,6 +2577,10 @@ var GanttChart = class {
|
|
|
2214
2577
|
}
|
|
2215
2578
|
this.#render();
|
|
2216
2579
|
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Removes the chart DOM and internal listeners, rendering the instance
|
|
2582
|
+
* unusable. Subsequent calls to any public method will throw.
|
|
2583
|
+
*/
|
|
2217
2584
|
destroy() {
|
|
2218
2585
|
if (this.#destroyed) return;
|
|
2219
2586
|
this.#destroyed = true;
|
|
@@ -2229,6 +2596,7 @@ var GanttChart = class {
|
|
|
2229
2596
|
clearChildren(this.#container);
|
|
2230
2597
|
}
|
|
2231
2598
|
#patchTask(id, patch) {
|
|
2599
|
+
if (this.#input === null) return;
|
|
2232
2600
|
const index = this.#taskIndex.get(id);
|
|
2233
2601
|
if (index === void 0) return;
|
|
2234
2602
|
const target = this.#input.tasks[index];
|
|
@@ -2246,7 +2614,7 @@ var GanttChart = class {
|
|
|
2246
2614
|
this.#cbs.onTaskEditIntent?.({
|
|
2247
2615
|
id: payload.id,
|
|
2248
2616
|
source: "grid",
|
|
2249
|
-
trigger: "
|
|
2617
|
+
trigger: "doubleClick",
|
|
2250
2618
|
task: payload.task
|
|
2251
2619
|
});
|
|
2252
2620
|
return;
|
|
@@ -2278,13 +2646,13 @@ var GanttChart = class {
|
|
|
2278
2646
|
this.#leftPane.style.maxWidth = `${computedWidth}px`;
|
|
2279
2647
|
this.#rightPane.style.minWidth = `${this.#timelineMinWidth}px`;
|
|
2280
2648
|
};
|
|
2281
|
-
#computeState() {
|
|
2282
|
-
const allRows = flattenTree(buildTaskTree(
|
|
2283
|
-
const [vpStart, vpEnd] = this.#opts.viewportStart !== void 0 && this.#opts.viewportEnd !== void 0 ? [this.#opts.viewportStart, this.#opts.viewportEnd] : deriveViewport(allRows,
|
|
2649
|
+
#computeState(input) {
|
|
2650
|
+
const allRows = flattenTree(buildTaskTree(input.tasks), this.#expandedIds);
|
|
2651
|
+
const [vpStart, vpEnd] = this.#opts.viewportStart !== void 0 && this.#opts.viewportEnd !== void 0 ? [this.#opts.viewportStart, this.#opts.viewportEnd] : deriveViewport(allRows, 48);
|
|
2284
2652
|
const mapper = createPixelMapper(this.#scale, vpStart);
|
|
2285
2653
|
const totalWidth = Math.ceil(mapper.toX(vpEnd)) + 1;
|
|
2286
2654
|
const layouts = computeLayout(allRows, mapper);
|
|
2287
|
-
const links = routeLinks(
|
|
2655
|
+
const links = routeLinks(input.links, layouts);
|
|
2288
2656
|
const containerH = this.#height - HEADER_H;
|
|
2289
2657
|
const rowCount = allRows.length;
|
|
2290
2658
|
const startIndex = Math.max(0, Math.floor(this.#scrollTop / ROW_HEIGHT) - OVERSCAN);
|
|
@@ -2292,7 +2660,7 @@ var GanttChart = class {
|
|
|
2292
2660
|
const paddingTop = startIndex * ROW_HEIGHT;
|
|
2293
2661
|
const paddingBottom = Math.max(0, (rowCount - 1 - endIndex) * ROW_HEIGHT);
|
|
2294
2662
|
return {
|
|
2295
|
-
input
|
|
2663
|
+
input,
|
|
2296
2664
|
scale: this.#scale,
|
|
2297
2665
|
highlightLinkedDependenciesOnSelect: this.#opts.highlightLinkedDependenciesOnSelect ?? false,
|
|
2298
2666
|
linkCreationEnabled: this.#opts.linkCreationEnabled ?? false,
|
|
@@ -2318,7 +2686,9 @@ var GanttChart = class {
|
|
|
2318
2686
|
}
|
|
2319
2687
|
#render = () => {
|
|
2320
2688
|
this.#rafPending = false;
|
|
2321
|
-
const
|
|
2689
|
+
const input = this.#input;
|
|
2690
|
+
if (input === null) return;
|
|
2691
|
+
const state = this.#computeState(input);
|
|
2322
2692
|
renderTimeHeader(this.#rightHeader, state);
|
|
2323
2693
|
renderLeftPane(this.#leftBody, state, {
|
|
2324
2694
|
onToggle: (id) => {
|
|
@@ -2457,6 +2827,6 @@ var GanttChart = class {
|
|
|
2457
2827
|
}
|
|
2458
2828
|
};
|
|
2459
2829
|
//#endregion
|
|
2460
|
-
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, TaskSchema, TaskTypeSchema, addDays, buildTaskTree, computeLayout, createPixelMapper, deriveViewport, deriveWeekNumbering, deriveWeekStartsOn, deriveWeekendDays, detectCycles, diffDays, flattenTree, formatLabel, formatWeekNumber, gridColumnDefaults, gridNaturalWidth, gridTemplateColumns, isParent, parseDate, parseGanttInput, resolveChartLocale, routeLinks, safeParseGanttInput, validateLinkRefs, visibleColumns };
|
|
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, TaskSchema, TaskTypeSchema, addDays, addHours, buildTaskTree, computeLayout, createPixelMapper, deriveViewport, deriveWeekNumbering, deriveWeekStartsOn, deriveWeekendDays, detectCycles, diffDays, diffHours, flattenTree, formatLabel, formatWeekNumber, gridColumnDefaults, gridNaturalWidth, gridTemplateColumns, isParent, parseDate, parseGanttInput, resolveChartLocale, routeLinks, safeParseGanttInput, validateLinkRefs, visibleColumns };
|
|
2461
2831
|
|
|
2462
2832
|
//# sourceMappingURL=index.mjs.map
|