@dou-ui/gantt 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/collaboration/index.js +323 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/peers.js +78 -0
- package/dist/collaboration/peers.js.map +1 -0
- package/dist/components/cell-editor.js +6 -0
- package/dist/components/cell-editor.js.map +1 -0
- package/dist/components/cell-editor.vue_vue_type_script_setup_true_lang.js +114 -0
- package/dist/components/cell-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/gantt-menu.js +6 -0
- package/dist/components/gantt-menu.js.map +1 -0
- package/dist/components/gantt-menu.vue_vue_type_script_setup_true_lang.js +40 -0
- package/dist/components/gantt-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/time-grid.js +6 -0
- package/dist/components/time-grid.js.map +1 -0
- package/dist/components/time-grid.vue_vue_type_script_setup_true_lang.js +32 -0
- package/dist/components/time-grid.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/time-header.js +6 -0
- package/dist/components/time-header.js.map +1 -0
- package/dist/components/time-header.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/time-header.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/context.js +12 -0
- package/dist/context.js.map +1 -0
- package/dist/core/baseline.js +48 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/calendar.js +191 -0
- package/dist/core/calendar.js.map +1 -0
- package/dist/core/csv.js +424 -0
- package/dist/core/csv.js.map +1 -0
- package/dist/core/document.js +231 -0
- package/dist/core/document.js.map +1 -0
- package/dist/core/extension.js +42 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/geometry.js +112 -0
- package/dist/core/geometry.js.map +1 -0
- package/dist/core/history.js +70 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/links.js +72 -0
- package/dist/core/links.js.map +1 -0
- package/dist/core/model.js +52 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/order.js +34 -0
- package/dist/core/order.js.map +1 -0
- package/dist/core/scale.js +118 -0
- package/dist/core/scale.js.map +1 -0
- package/dist/core/schedule.js +235 -0
- package/dist/core/schedule.js.map +1 -0
- package/dist/core/time.js +81 -0
- package/dist/core/time.js.map +1 -0
- package/dist/core/transaction.js +371 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/core/tree.js +114 -0
- package/dist/core/tree.js.map +1 -0
- package/dist/export/draw.js +214 -0
- package/dist/export/draw.js.map +1 -0
- package/dist/export/theme.js +131 -0
- package/dist/export/theme.js.map +1 -0
- package/dist/gantt.js +6 -0
- package/dist/gantt.js.map +1 -0
- package/dist/gantt.vue_vue_type_script_setup_true_lang.js +2774 -0
- package/dist/gantt.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.js +87 -0
- package/dist/labels.js.map +1 -0
- package/dist/locale.js +355 -0
- package/dist/locale.js.map +1 -0
- package/dist/scheduler/index.js +8 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/layout.js +97 -0
- package/dist/scheduler/layout.js.map +1 -0
- package/dist/scheduler/scheduler.js +6 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/scheduler.vue_vue_type_script_setup_true_lang.js +1359 -0
- package/dist/scheduler/scheduler.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/scheduler/style.d.ts +1 -0
- package/dist/scheduler/style.js +2 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +4 -0
- package/dist/time-axis.js +337 -0
- package/dist/time-axis.js.map +1 -0
- package/dist/types/collaboration/index.d.ts +39 -0
- package/dist/types/collaboration/peers.d.ts +22 -0
- package/dist/types/components/cell-editor.vue.d.ts +21 -0
- package/dist/types/components/gantt-menu.vue.d.ts +18 -0
- package/dist/types/components/time-grid.vue.d.ts +12 -0
- package/dist/types/components/time-header.vue.d.ts +23 -0
- package/dist/types/context.d.ts +64 -0
- package/dist/types/core/baseline.d.ts +14 -0
- package/dist/types/core/calendar.d.ts +75 -0
- package/dist/types/core/csv.d.ts +77 -0
- package/dist/types/core/document.d.ts +51 -0
- package/dist/types/core/extension.d.ts +88 -0
- package/dist/types/core/geometry.d.ts +33 -0
- package/dist/types/core/history.d.ts +19 -0
- package/dist/types/core/links.d.ts +20 -0
- package/dist/types/core/menu.d.ts +27 -0
- package/dist/types/core/model.d.ts +72 -0
- package/dist/types/core/order.d.ts +9 -0
- package/dist/types/core/scale.d.ts +33 -0
- package/dist/types/core/schedule.d.ts +44 -0
- package/dist/types/core/time.d.ts +39 -0
- package/dist/types/core/transaction.d.ts +134 -0
- package/dist/types/core/tree.d.ts +30 -0
- package/dist/types/export/draw.d.ts +76 -0
- package/dist/types/export/theme.d.ts +49 -0
- package/dist/types/gantt.vue.d.ts +54 -0
- package/dist/types/index.d.ts +148 -0
- package/dist/types/labels.d.ts +24 -0
- package/dist/types/locale.d.ts +161 -0
- package/dist/types/scheduler/index.d.ts +102 -0
- package/dist/types/scheduler/layout.d.ts +58 -0
- package/dist/types/scheduler/scheduler.vue.d.ts +46 -0
- package/dist/types/scheduler/types.d.ts +109 -0
- package/dist/types/time-axis.d.ts +80 -0
- package/dist/types/types.d.ts +305 -0
- package/package.json +79 -4
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { MINUTES_PER_DAY, parseTime } from "./time.js";
|
|
2
|
+
import { writeSpan } from "./model.js";
|
|
3
|
+
import { buildTree } from "./tree.js";
|
|
4
|
+
import { isOrderKey, orderBetween, orderKeys } from "./order.js";
|
|
5
|
+
// src/core/transaction.ts
|
|
6
|
+
/** An empty document, the chart's `v-model` when none is bound. */
|
|
7
|
+
var emptyDocument = () => ({
|
|
8
|
+
version: 1,
|
|
9
|
+
tasks: [],
|
|
10
|
+
links: []
|
|
11
|
+
});
|
|
12
|
+
function patched(item, patch) {
|
|
13
|
+
const next = { ...item };
|
|
14
|
+
for (const key of Object.keys(patch)) {
|
|
15
|
+
const value = patch[key];
|
|
16
|
+
if (value === void 0) delete next[key];
|
|
17
|
+
else next[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
/** The values `patch` would replace, so applying them restores `item`. */
|
|
22
|
+
function previous(item, patch) {
|
|
23
|
+
const old = {};
|
|
24
|
+
for (const key of Object.keys(patch)) old[key] = item[key];
|
|
25
|
+
return old;
|
|
26
|
+
}
|
|
27
|
+
/** A time as minutes; a date-only end counts its whole day. */
|
|
28
|
+
function minutesOf(time, end) {
|
|
29
|
+
if (typeof time === "number") return time;
|
|
30
|
+
const parsed = parseTime(time);
|
|
31
|
+
if (!parsed) throw new RangeError(`[@dou-ui/gantt] can't read the time "${time}"`);
|
|
32
|
+
return parsed.at + (end && parsed.dateOnly ? MINUTES_PER_DAY : 0);
|
|
33
|
+
}
|
|
34
|
+
var clampIndex = (index, length) => index == null ? length : Math.max(0, Math.min(length, index));
|
|
35
|
+
/**
|
|
36
|
+
* A batch of changes. Steps apply as they are added, so later calls see
|
|
37
|
+
* earlier ones; `doc` is the result.
|
|
38
|
+
*/
|
|
39
|
+
var GanttTransaction = class {
|
|
40
|
+
before;
|
|
41
|
+
steps = [];
|
|
42
|
+
/** The steps that undo this transaction, in the order to apply them. */
|
|
43
|
+
inverse = [];
|
|
44
|
+
meta = {};
|
|
45
|
+
tasks;
|
|
46
|
+
links;
|
|
47
|
+
copied = {
|
|
48
|
+
tasks: false,
|
|
49
|
+
links: false
|
|
50
|
+
};
|
|
51
|
+
/** Where each task and link is in its list; built on first use, dropped when a list changes length. */
|
|
52
|
+
at = {
|
|
53
|
+
tasks: null,
|
|
54
|
+
links: null
|
|
55
|
+
};
|
|
56
|
+
constructor(before) {
|
|
57
|
+
this.before = before;
|
|
58
|
+
this.tasks = before.tasks;
|
|
59
|
+
this.links = before.links;
|
|
60
|
+
}
|
|
61
|
+
get doc() {
|
|
62
|
+
if (!this.steps.length) return this.before;
|
|
63
|
+
return {
|
|
64
|
+
...this.before,
|
|
65
|
+
tasks: this.tasks,
|
|
66
|
+
links: this.links
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Whether the change makes any change: it has steps and wasn't cancelled. */
|
|
70
|
+
get changed() {
|
|
71
|
+
return this.steps.length > 0 && !this.stopped;
|
|
72
|
+
}
|
|
73
|
+
stopped = null;
|
|
74
|
+
/**
|
|
75
|
+
* Stops the change: none of it is applied or kept for undo, and the
|
|
76
|
+
* editor fires `transactionCancel` and says why. For an extension's `transaction`
|
|
77
|
+
* hook, e.g. to keep people from changing what they may not; hooks after
|
|
78
|
+
* it don't run. A change kept out of the history (`meta.history: false`),
|
|
79
|
+
* such as undo, redo or another person's, can't be cancelled and goes ahead.
|
|
80
|
+
*/
|
|
81
|
+
cancel(reason) {
|
|
82
|
+
if (this.meta.history !== false) this.stopped = { reason };
|
|
83
|
+
else warnUncancellable();
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** Whether `cancel` stopped it. */
|
|
87
|
+
get cancelled() {
|
|
88
|
+
return this.stopped !== null;
|
|
89
|
+
}
|
|
90
|
+
/** What `cancel` was given, to tell people why. */
|
|
91
|
+
get cancelReason() {
|
|
92
|
+
return this.stopped?.reason;
|
|
93
|
+
}
|
|
94
|
+
/** Whether the tasks, or the links, changed. */
|
|
95
|
+
touches(kind) {
|
|
96
|
+
return this.copied[kind];
|
|
97
|
+
}
|
|
98
|
+
getTask(id) {
|
|
99
|
+
return this.tasks[this.indexOf("tasks", id)];
|
|
100
|
+
}
|
|
101
|
+
getLink(id) {
|
|
102
|
+
return this.links[this.indexOf("links", id)];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A task's or link's position in its list, `-1` when there is none. The
|
|
106
|
+
* positions are kept as tasks and links come and go, and checked when
|
|
107
|
+
* read: one that moved since is found again with the rest.
|
|
108
|
+
*/
|
|
109
|
+
indexOf(kind, id) {
|
|
110
|
+
const list = kind === "tasks" ? this.tasks : this.links;
|
|
111
|
+
let at = this.at[kind];
|
|
112
|
+
const known = at?.get(id);
|
|
113
|
+
if (at && (known === void 0 || list[known]?.id === id)) return known ?? -1;
|
|
114
|
+
at = /* @__PURE__ */ new Map();
|
|
115
|
+
for (let i = list.length - 1; i >= 0; i--) at.set(list[i].id, i);
|
|
116
|
+
this.at[kind] = at;
|
|
117
|
+
return at.get(id) ?? -1;
|
|
118
|
+
}
|
|
119
|
+
/** Links into or out of a task. */
|
|
120
|
+
linksOf(id) {
|
|
121
|
+
return this.links.filter((link) => link.from === id || link.to === id);
|
|
122
|
+
}
|
|
123
|
+
setMeta(meta) {
|
|
124
|
+
Object.assign(this.meta, meta);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
step(step) {
|
|
128
|
+
const inverse = this.apply(step);
|
|
129
|
+
if (inverse) {
|
|
130
|
+
this.steps.push(step);
|
|
131
|
+
this.inverse.unshift(inverse);
|
|
132
|
+
}
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
addTask(task, index) {
|
|
136
|
+
return this.step({
|
|
137
|
+
type: "addTask",
|
|
138
|
+
task,
|
|
139
|
+
index
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Removes a task with its subtasks and every link attached to them. */
|
|
143
|
+
removeTask(id) {
|
|
144
|
+
for (const child of this.tasks.filter((task) => task.parent === id)) this.removeTask(child.id);
|
|
145
|
+
for (const link of this.linksOf(id)) this.removeLink(link.id);
|
|
146
|
+
return this.step({
|
|
147
|
+
type: "removeTask",
|
|
148
|
+
id
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
updateTask(id, patch) {
|
|
152
|
+
return this.step({
|
|
153
|
+
type: "updateTask",
|
|
154
|
+
id,
|
|
155
|
+
patch
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sets a task's dates, written back in the task's own style. Takes
|
|
160
|
+
* minutes, or strings read like the task's own: a date-only `end` is the
|
|
161
|
+
* last day of the task.
|
|
162
|
+
*/
|
|
163
|
+
setSpan(id, start, end) {
|
|
164
|
+
const task = this.getTask(id);
|
|
165
|
+
if (!task) return this;
|
|
166
|
+
return this.updateTask(id, writeSpan(task, minutesOf(start, false), minutesOf(end, true)));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Moves a task, with its subtasks, under `parent` (or to the top level)
|
|
170
|
+
* at `index` among its new siblings, the end by default. Only the task's
|
|
171
|
+
* `parent` and `order` change, unless its new siblings have no room
|
|
172
|
+
* between them: then they all get new `order` keys. Links between the
|
|
173
|
+
* task's subtasks and its new parents go, since a task can't depend on
|
|
174
|
+
* its own parent. A move into the task itself is ignored.
|
|
175
|
+
*/
|
|
176
|
+
moveTask(id, parent, index) {
|
|
177
|
+
if (!this.getTask(id)) return this;
|
|
178
|
+
if (parent != null && !this.getTask(parent)) return this;
|
|
179
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
180
|
+
for (let up = parent; up != null && !ancestors.has(up); up = this.getTask(up)?.parent) {
|
|
181
|
+
if (up === id) return this;
|
|
182
|
+
ancestors.add(up);
|
|
183
|
+
}
|
|
184
|
+
const tree = buildTree(this.tasks);
|
|
185
|
+
const siblings = (tree.childrenOf.get(parent) ?? []).filter((task) => task.id !== id);
|
|
186
|
+
const at = clampIndex(index, siblings.length);
|
|
187
|
+
const keys = siblings.map((task) => task.order);
|
|
188
|
+
const ordered = keys.every((key, i) => key != null && isOrderKey(key) && (i === 0 || keys[i - 1] < key));
|
|
189
|
+
let order;
|
|
190
|
+
if (ordered) order = orderBetween(keys[at - 1], keys[at]);
|
|
191
|
+
else {
|
|
192
|
+
const fresh = orderKeys(siblings.length + 1);
|
|
193
|
+
order = fresh[at];
|
|
194
|
+
siblings.forEach((task, i) => this.updateTask(task.id, { order: fresh[i < at ? i : i + 1] }));
|
|
195
|
+
}
|
|
196
|
+
this.updateTask(id, {
|
|
197
|
+
parent,
|
|
198
|
+
order
|
|
199
|
+
});
|
|
200
|
+
const moved = /* @__PURE__ */ new Set([id]);
|
|
201
|
+
const stack = [id];
|
|
202
|
+
while (stack.length) for (const child of tree.childrenOf.get(stack.pop()) ?? []) {
|
|
203
|
+
moved.add(child.id);
|
|
204
|
+
stack.push(child.id);
|
|
205
|
+
}
|
|
206
|
+
for (const link of this.links.slice()) if (moved.has(link.from) && ancestors.has(link.to) || moved.has(link.to) && ancestors.has(link.from)) this.removeLink(link.id);
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
/** Merges `patch` into the task's `data`. */
|
|
210
|
+
updateData(id, patch) {
|
|
211
|
+
const task = this.getTask(id);
|
|
212
|
+
if (!task) return this;
|
|
213
|
+
return this.updateTask(id, { data: {
|
|
214
|
+
...task.data,
|
|
215
|
+
...patch
|
|
216
|
+
} });
|
|
217
|
+
}
|
|
218
|
+
addLink(link, index) {
|
|
219
|
+
return this.step({
|
|
220
|
+
type: "addLink",
|
|
221
|
+
link,
|
|
222
|
+
index
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
removeLink(id) {
|
|
226
|
+
return this.step({
|
|
227
|
+
type: "removeLink",
|
|
228
|
+
id
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
updateLink(id, patch) {
|
|
232
|
+
return this.step({
|
|
233
|
+
type: "updateLink",
|
|
234
|
+
id,
|
|
235
|
+
patch
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
writable(kind) {
|
|
239
|
+
if (!this.copied[kind]) {
|
|
240
|
+
this.copied[kind] = true;
|
|
241
|
+
if (kind === "tasks") this.tasks = this.tasks.slice();
|
|
242
|
+
else this.links = this.links.slice();
|
|
243
|
+
}
|
|
244
|
+
return kind === "tasks" ? this.tasks : this.links;
|
|
245
|
+
}
|
|
246
|
+
/** Applies a step and returns its inverse, or `null` when it changes nothing. */
|
|
247
|
+
apply(step) {
|
|
248
|
+
switch (step.type) {
|
|
249
|
+
case "addTask": {
|
|
250
|
+
if (this.getTask(step.task.id)) return null;
|
|
251
|
+
const list = this.writable("tasks");
|
|
252
|
+
const at = clampIndex(step.index, list.length);
|
|
253
|
+
list.splice(at, 0, step.task);
|
|
254
|
+
this.at.tasks?.set(step.task.id, at);
|
|
255
|
+
return {
|
|
256
|
+
type: "removeTask",
|
|
257
|
+
id: step.task.id
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
case "removeTask": {
|
|
261
|
+
const index = this.indexOf("tasks", step.id);
|
|
262
|
+
if (index < 0) return null;
|
|
263
|
+
const [task] = this.writable("tasks").splice(index, 1);
|
|
264
|
+
return {
|
|
265
|
+
type: "addTask",
|
|
266
|
+
task,
|
|
267
|
+
index
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
case "updateTask": {
|
|
271
|
+
const index = this.indexOf("tasks", step.id);
|
|
272
|
+
if (index < 0) return null;
|
|
273
|
+
const task = this.tasks[index];
|
|
274
|
+
if (Object.keys(step.patch).every((key) => task[key] === step.patch[key])) return null;
|
|
275
|
+
this.writable("tasks")[index] = patched(task, step.patch);
|
|
276
|
+
return {
|
|
277
|
+
type: "updateTask",
|
|
278
|
+
id: step.id,
|
|
279
|
+
patch: previous(task, step.patch)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
case "addLink": {
|
|
283
|
+
if (this.getLink(step.link.id)) return null;
|
|
284
|
+
const list = this.writable("links");
|
|
285
|
+
const at = clampIndex(step.index, list.length);
|
|
286
|
+
list.splice(at, 0, step.link);
|
|
287
|
+
this.at.links?.set(step.link.id, at);
|
|
288
|
+
return {
|
|
289
|
+
type: "removeLink",
|
|
290
|
+
id: step.link.id
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "removeLink": {
|
|
294
|
+
const index = this.indexOf("links", step.id);
|
|
295
|
+
if (index < 0) return null;
|
|
296
|
+
const [link] = this.writable("links").splice(index, 1);
|
|
297
|
+
return {
|
|
298
|
+
type: "addLink",
|
|
299
|
+
link,
|
|
300
|
+
index
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
case "updateLink": {
|
|
304
|
+
const index = this.indexOf("links", step.id);
|
|
305
|
+
if (index < 0) return null;
|
|
306
|
+
const link = this.links[index];
|
|
307
|
+
if (Object.keys(step.patch).every((key) => link[key] === step.patch[key])) return null;
|
|
308
|
+
this.writable("links")[index] = patched(link, step.patch);
|
|
309
|
+
return {
|
|
310
|
+
type: "updateLink",
|
|
311
|
+
id: step.id,
|
|
312
|
+
patch: previous(link, step.patch)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
/** Applies steps to a document; steps that no longer apply are skipped. */
|
|
319
|
+
function applyGanttSteps(doc, steps) {
|
|
320
|
+
const tr = new GanttTransaction(doc);
|
|
321
|
+
for (const step of steps) tr.step(step);
|
|
322
|
+
return tr;
|
|
323
|
+
}
|
|
324
|
+
var targetOf = (step) => {
|
|
325
|
+
switch (step.type) {
|
|
326
|
+
case "addTask": return `t:${step.task.id}`;
|
|
327
|
+
case "addLink": return `l:${step.link.id}`;
|
|
328
|
+
case "removeTask":
|
|
329
|
+
case "updateTask": return `t:${step.id}`;
|
|
330
|
+
case "removeLink":
|
|
331
|
+
case "updateLink": return `l:${step.id}`;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Folds updates of the same task or link into one, as long as nothing
|
|
336
|
+
* adds or removes it in between.
|
|
337
|
+
*/
|
|
338
|
+
function compactSteps(steps) {
|
|
339
|
+
const out = [];
|
|
340
|
+
const lastUpdate = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const step of steps) {
|
|
342
|
+
const target = targetOf(step);
|
|
343
|
+
if (step.type === "updateTask" || step.type === "updateLink") {
|
|
344
|
+
const at = lastUpdate.get(target);
|
|
345
|
+
if (at != null) {
|
|
346
|
+
const prev = out[at];
|
|
347
|
+
out[at] = {
|
|
348
|
+
...prev,
|
|
349
|
+
patch: {
|
|
350
|
+
...prev.patch,
|
|
351
|
+
...step.patch
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
lastUpdate.set(target, out.length);
|
|
357
|
+
} else lastUpdate.delete(target);
|
|
358
|
+
out.push(step);
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
var warnedUncancellable = false;
|
|
363
|
+
/** Says once that cancelling a change kept out of the history does nothing. */
|
|
364
|
+
function warnUncancellable() {
|
|
365
|
+
if (process.env.NODE_ENV === "production" || warnedUncancellable) return;
|
|
366
|
+
warnedUncancellable = true;
|
|
367
|
+
console.warn("[@dou-ui/gantt] a change kept out of the history (undo, redo or another person's) can't be cancelled; it goes ahead");
|
|
368
|
+
}
|
|
369
|
+
export { GanttTransaction, applyGanttSteps, compactSteps, emptyDocument };
|
|
370
|
+
|
|
371
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","names":[],"sources":["../../src/core/transaction.ts"],"sourcesContent":["import { writeSpan, type GanttLink, type GanttTask } from './model'\nimport { isOrderKey, orderBetween, orderKeys } from './order'\nimport { MINUTES_PER_DAY, parseTime } from './time'\nimport { buildTree } from './tree'\n\n/*\n * Every change to the tasks and links is a list of steps. Applying a step\n * also gives the step that undoes it, so history, collaboration and the\n * `transaction` event all work on the same small vocabulary.\n */\n\n/** An empty document, the chart's `v-model` when none is bound. */\nexport const emptyDocument = (): GanttDocument => ({ version: 1, tasks: [], links: [] })\n\ntype Patch<T> = { [K in keyof T]?: T[K] | undefined }\n\n/** The tasks and links together, as a transaction sees them. */\nexport interface GanttDocument {\n /** The version of the format; see `migrateDocument`. */\n version: 1\n tasks: GanttTask[]\n links: GanttLink[]\n}\n\nexport type GanttStep =\n | { type: 'addTask'; task: GanttTask; index?: number }\n | { type: 'removeTask'; id: string }\n | { type: 'updateTask'; id: string; patch: Patch<GanttTask> }\n | { type: 'addLink'; link: GanttLink; index?: number }\n | { type: 'removeLink'; id: string }\n | { type: 'updateLink'; id: string; patch: Patch<GanttLink> }\n\nexport interface GanttTransactionMeta {\n /** Keep out of the undo history, e.g. folding a summary or a change from another client. */\n history?: boolean\n /**\n * Consecutive transactions in the same group within a second undo as\n * one, e.g. repeated arrow key nudges.\n */\n group?: string\n /**\n * With `scheduling=\"auto\"`, set to `false` to leave the waiting tasks\n * where they are, e.g. for a change from another client.\n */\n schedule?: boolean\n /** Who made the change, passed on to the `transaction` event. */\n origin?: string\n}\n\nfunction patched<T extends object>(item: T, patch: Patch<T>): T {\n const next = { ...item } as Record<string, unknown>\n for (const key of Object.keys(patch)) {\n const value = (patch as Record<string, unknown>)[key]\n if (value === undefined) delete next[key]\n else next[key] = value\n }\n return next as T\n}\n\n/** The values `patch` would replace, so applying them restores `item`. */\nfunction previous<T extends object>(item: T, patch: Patch<T>): Patch<T> {\n const old: Record<string, unknown> = {}\n for (const key of Object.keys(patch)) old[key] = (item as Record<string, unknown>)[key]\n return old as Patch<T>\n}\n\n/** A time as minutes; a date-only end counts its whole day. */\nfunction minutesOf(time: number | string, end: boolean) {\n if (typeof time === 'number') return time\n const parsed = parseTime(time)\n if (!parsed) throw new RangeError(`[@dou-ui/gantt] can't read the time \"${time}\"`)\n return parsed.at + (end && parsed.dateOnly ? MINUTES_PER_DAY : 0)\n}\n\nconst clampIndex = (index: number | undefined, length: number) =>\n index == null ? length : Math.max(0, Math.min(length, index))\n\n/**\n * A batch of changes. Steps apply as they are added, so later calls see\n * earlier ones; `doc` is the result.\n */\nexport class GanttTransaction {\n readonly steps: GanttStep[] = []\n /** The steps that undo this transaction, in the order to apply them. */\n readonly inverse: GanttStep[] = []\n meta: GanttTransactionMeta = {}\n private tasks: GanttTask[]\n private links: GanttLink[]\n private copied = { tasks: false, links: false }\n /** Where each task and link is in its list; built on first use, dropped when a list changes length. */\n private at: { tasks: Map<string, number> | null; links: Map<string, number> | null } = {\n tasks: null,\n links: null,\n }\n\n constructor(readonly before: GanttDocument) {\n this.tasks = before.tasks\n this.links = before.links\n }\n\n get doc(): GanttDocument {\n if (!this.steps.length) return this.before\n return { ...this.before, tasks: this.tasks, links: this.links }\n }\n\n /** Whether the change makes any change: it has steps and wasn't cancelled. */\n get changed() {\n return this.steps.length > 0 && !this.stopped\n }\n\n private stopped: { reason?: string } | null = null\n\n /**\n * Stops the change: none of it is applied or kept for undo, and the\n * editor fires `transactionCancel` and says why. For an extension's `transaction`\n * hook, e.g. to keep people from changing what they may not; hooks after\n * it don't run. A change kept out of the history (`meta.history: false`),\n * such as undo, redo or another person's, can't be cancelled and goes ahead.\n */\n cancel(reason?: string) {\n if (this.meta.history !== false) this.stopped = { reason }\n else warnUncancellable()\n return this\n }\n\n /** Whether `cancel` stopped it. */\n get cancelled() {\n return this.stopped !== null\n }\n\n /** What `cancel` was given, to tell people why. */\n get cancelReason() {\n return this.stopped?.reason\n }\n\n /** Whether the tasks, or the links, changed. */\n touches(kind: 'tasks' | 'links') {\n return this.copied[kind]\n }\n\n getTask(id: string) {\n return this.tasks[this.indexOf('tasks', id)]\n }\n\n getLink(id: string) {\n return this.links[this.indexOf('links', id)]\n }\n\n /**\n * A task's or link's position in its list, `-1` when there is none. The\n * positions are kept as tasks and links come and go, and checked when\n * read: one that moved since is found again with the rest.\n */\n private indexOf(kind: 'tasks' | 'links', id: string) {\n const list: readonly { id: string }[] = kind === 'tasks' ? this.tasks : this.links\n let at = this.at[kind]\n const known = at?.get(id)\n if (at && (known === undefined || list[known]?.id === id)) return known ?? -1\n at = new Map()\n // the first one wins, as with `find`\n for (let i = list.length - 1; i >= 0; i--) at.set(list[i]!.id, i)\n this.at[kind] = at\n return at.get(id) ?? -1\n }\n\n /** Links into or out of a task. */\n linksOf(id: string) {\n return this.links.filter((link) => link.from === id || link.to === id)\n }\n\n setMeta(meta: GanttTransactionMeta) {\n Object.assign(this.meta, meta)\n return this\n }\n\n step(step: GanttStep) {\n const inverse = this.apply(step)\n if (inverse) {\n this.steps.push(step)\n this.inverse.unshift(inverse)\n }\n return this\n }\n\n addTask(task: GanttTask, index?: number) {\n return this.step({ type: 'addTask', task, index })\n }\n\n /** Removes a task with its subtasks and every link attached to them. */\n removeTask(id: string) {\n for (const child of this.tasks.filter((task) => task.parent === id)) this.removeTask(child.id)\n for (const link of this.linksOf(id)) this.removeLink(link.id)\n return this.step({ type: 'removeTask', id })\n }\n\n updateTask(id: string, patch: Patch<GanttTask>) {\n return this.step({ type: 'updateTask', id, patch })\n }\n\n /**\n * Sets a task's dates, written back in the task's own style. Takes\n * minutes, or strings read like the task's own: a date-only `end` is the\n * last day of the task.\n */\n setSpan(id: string, start: number | string, end: number | string) {\n const task = this.getTask(id)\n if (!task) return this\n return this.updateTask(id, writeSpan(task, minutesOf(start, false), minutesOf(end, true)))\n }\n\n /**\n * Moves a task, with its subtasks, under `parent` (or to the top level)\n * at `index` among its new siblings, the end by default. Only the task's\n * `parent` and `order` change, unless its new siblings have no room\n * between them: then they all get new `order` keys. Links between the\n * task's subtasks and its new parents go, since a task can't depend on\n * its own parent. A move into the task itself is ignored.\n */\n moveTask(id: string, parent: string | undefined, index?: number) {\n if (!this.getTask(id)) return this\n if (parent != null && !this.getTask(parent)) return this\n const ancestors = new Set<string>()\n for (let up = parent; up != null && !ancestors.has(up); up = this.getTask(up)?.parent) {\n if (up === id) return this\n ancestors.add(up)\n }\n const tree = buildTree(this.tasks)\n const siblings = (tree.childrenOf.get(parent) ?? []).filter((task) => task.id !== id)\n const at = clampIndex(index, siblings.length)\n const keys = siblings.map((task) => task.order)\n const ordered = keys.every(\n (key, i) => key != null && isOrderKey(key) && (i === 0 || keys[i - 1]! < key),\n )\n let order: string\n if (ordered) order = orderBetween(keys[at - 1], keys[at])\n else {\n const fresh = orderKeys(siblings.length + 1)\n order = fresh[at]!\n siblings.forEach((task, i) => this.updateTask(task.id, { order: fresh[i < at ? i : i + 1] }))\n }\n this.updateTask(id, { parent, order })\n\n // the task and everything under it\n const moved = new Set([id])\n const stack = [id]\n while (stack.length) {\n for (const child of tree.childrenOf.get(stack.pop()) ?? []) {\n moved.add(child.id)\n stack.push(child.id)\n }\n }\n for (const link of this.links.slice()) {\n if (\n (moved.has(link.from) && ancestors.has(link.to)) ||\n (moved.has(link.to) && ancestors.has(link.from))\n )\n this.removeLink(link.id)\n }\n return this\n }\n\n /** Merges `patch` into the task's `data`. */\n updateData(id: string, patch: Record<string, unknown>) {\n const task = this.getTask(id)\n if (!task) return this\n return this.updateTask(id, { data: { ...task.data, ...patch } })\n }\n\n addLink(link: GanttLink, index?: number) {\n return this.step({ type: 'addLink', link, index })\n }\n\n removeLink(id: string) {\n return this.step({ type: 'removeLink', id })\n }\n\n updateLink(id: string, patch: Patch<GanttLink>) {\n return this.step({ type: 'updateLink', id, patch })\n }\n\n private writable<K extends 'tasks' | 'links'>(kind: K): GanttDocument[K] {\n if (!this.copied[kind]) {\n this.copied[kind] = true\n if (kind === 'tasks') this.tasks = this.tasks.slice()\n else this.links = this.links.slice()\n }\n return (kind === 'tasks' ? this.tasks : this.links) as GanttDocument[K]\n }\n\n /** Applies a step and returns its inverse, or `null` when it changes nothing. */\n private apply(step: GanttStep): GanttStep | null {\n switch (step.type) {\n case 'addTask': {\n if (this.getTask(step.task.id)) return null\n const list = this.writable('tasks')\n const at = clampIndex(step.index, list.length)\n list.splice(at, 0, step.task)\n // the rest may have moved by one: `indexOf` checks the positions it reads\n this.at.tasks?.set(step.task.id, at)\n return { type: 'removeTask', id: step.task.id }\n }\n case 'removeTask': {\n const index = this.indexOf('tasks', step.id)\n if (index < 0) return null\n const [task] = this.writable('tasks').splice(index, 1)\n return { type: 'addTask', task: task!, index }\n }\n case 'updateTask': {\n const index = this.indexOf('tasks', step.id)\n if (index < 0) return null\n const task = this.tasks[index]!\n const keys = Object.keys(step.patch) as (keyof GanttTask)[]\n if (keys.every((key) => task[key] === step.patch[key])) return null\n this.writable('tasks')[index] = patched(task, step.patch)\n return { type: 'updateTask', id: step.id, patch: previous(task, step.patch) }\n }\n case 'addLink': {\n if (this.getLink(step.link.id)) return null\n const list = this.writable('links')\n const at = clampIndex(step.index, list.length)\n list.splice(at, 0, step.link)\n // the rest may have moved by one: `indexOf` checks the positions it reads\n this.at.links?.set(step.link.id, at)\n return { type: 'removeLink', id: step.link.id }\n }\n case 'removeLink': {\n const index = this.indexOf('links', step.id)\n if (index < 0) return null\n const [link] = this.writable('links').splice(index, 1)\n return { type: 'addLink', link: link!, index }\n }\n case 'updateLink': {\n const index = this.indexOf('links', step.id)\n if (index < 0) return null\n const link = this.links[index]!\n const keys = Object.keys(step.patch) as (keyof GanttLink)[]\n if (keys.every((key) => link[key] === step.patch[key])) return null\n this.writable('links')[index] = patched(link, step.patch)\n return { type: 'updateLink', id: step.id, patch: previous(link, step.patch) }\n }\n }\n }\n}\n\n/** Applies steps to a document; steps that no longer apply are skipped. */\nexport function applyGanttSteps(doc: GanttDocument, steps: readonly GanttStep[]) {\n const tr = new GanttTransaction(doc)\n for (const step of steps) tr.step(step)\n return tr\n}\n\nconst targetOf = (step: GanttStep) => {\n switch (step.type) {\n case 'addTask':\n return `t:${step.task.id}`\n case 'addLink':\n return `l:${step.link.id}`\n case 'removeTask':\n case 'updateTask':\n return `t:${step.id}`\n case 'removeLink':\n case 'updateLink':\n return `l:${step.id}`\n }\n}\n\n/**\n * Folds updates of the same task or link into one, as long as nothing\n * adds or removes it in between.\n */\nexport function compactSteps(steps: readonly GanttStep[]): GanttStep[] {\n const out: GanttStep[] = []\n const lastUpdate = new Map<string, number>()\n for (const step of steps) {\n const target = targetOf(step)\n if (step.type === 'updateTask' || step.type === 'updateLink') {\n const at = lastUpdate.get(target)\n if (at != null) {\n const prev = out[at] as typeof step\n out[at] = { ...prev, patch: { ...prev.patch, ...step.patch } } as GanttStep\n continue\n }\n lastUpdate.set(target, out.length)\n } else {\n lastUpdate.delete(target)\n }\n out.push(step)\n }\n return out\n}\n\nlet warnedUncancellable = false\n/** Says once that cancelling a change kept out of the history does nothing. */\nfunction warnUncancellable() {\n // the app's bundler replaces this, as for Vue's own warnings\n if (process.env.NODE_ENV === 'production' || warnedUncancellable) return\n warnedUncancellable = true\n console.warn(\n \"[@dou-ui/gantt] a change kept out of the history (undo, redo or another person's) can't be cancelled; it goes ahead\",\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { clampProgress, spanOf } from "./model.js";
|
|
2
|
+
// src/core/tree.ts
|
|
3
|
+
/** Between two siblings given the same `order`: by id. */
|
|
4
|
+
var sameOrder = (a, b) => a.order == null || b.order == null || a.id === b.id ? 0 : a.id < b.id ? -1 : 1;
|
|
5
|
+
/**
|
|
6
|
+
* Orders siblings: by `order` where given, otherwise as in the array. A
|
|
7
|
+
* sibling without `order` sorts right after the one before it in the array.
|
|
8
|
+
* Two siblings given the same `order` (two people inserting at the same
|
|
9
|
+
* place) sort by id, so every client shows them alike.
|
|
10
|
+
*/
|
|
11
|
+
function sortSiblings(list) {
|
|
12
|
+
let key = "";
|
|
13
|
+
const keyed = list.map((item) => {
|
|
14
|
+
if (item.task.order != null) key = item.task.order;
|
|
15
|
+
return {
|
|
16
|
+
...item,
|
|
17
|
+
key
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : sameOrder(a.task, b.task) || a.index - b.index);
|
|
21
|
+
return keyed.map((item) => item.task);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lays the flat task list out as a tree: rows in display order, summaries
|
|
25
|
+
* worked out from their children. A `parent` that doesn't exist, or that
|
|
26
|
+
* would make a loop, is ignored and the task becomes a root.
|
|
27
|
+
*/
|
|
28
|
+
function buildTree(tasks) {
|
|
29
|
+
const ids = new Map(tasks.map((task) => [task.id, task]));
|
|
30
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const task of tasks) {
|
|
32
|
+
let parent = task.parent != null && ids.has(task.parent) ? task.parent : void 0;
|
|
33
|
+
const seen = /* @__PURE__ */ new Set([task.id]);
|
|
34
|
+
for (let up = parent; up != null;) {
|
|
35
|
+
if (seen.has(up)) {
|
|
36
|
+
parent = void 0;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
seen.add(up);
|
|
40
|
+
const next = ids.get(up).parent;
|
|
41
|
+
up = next != null && ids.has(next) ? next : void 0;
|
|
42
|
+
}
|
|
43
|
+
parentOf.set(task.id, parent);
|
|
44
|
+
}
|
|
45
|
+
const groups = /* @__PURE__ */ new Map();
|
|
46
|
+
tasks.forEach((task, index) => {
|
|
47
|
+
const parent = parentOf.get(task.id);
|
|
48
|
+
const list = groups.get(parent);
|
|
49
|
+
if (list) list.push({
|
|
50
|
+
task,
|
|
51
|
+
index
|
|
52
|
+
});
|
|
53
|
+
else groups.set(parent, [{
|
|
54
|
+
task,
|
|
55
|
+
index
|
|
56
|
+
}]);
|
|
57
|
+
});
|
|
58
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const [parent, list] of groups) childrenOf.set(parent, sortSiblings(list));
|
|
60
|
+
const byId = /* @__PURE__ */ new Map();
|
|
61
|
+
const rows = [];
|
|
62
|
+
const visit = (task, depth, shown) => {
|
|
63
|
+
const kids = childrenOf.get(task.id) ?? [];
|
|
64
|
+
const row = {
|
|
65
|
+
task,
|
|
66
|
+
depth,
|
|
67
|
+
index: shown ? rows.length : -1,
|
|
68
|
+
children: kids.length,
|
|
69
|
+
summary: kids.length > 0,
|
|
70
|
+
milestone: kids.length === 0 && task.type === "milestone",
|
|
71
|
+
span: null,
|
|
72
|
+
progress: 0
|
|
73
|
+
};
|
|
74
|
+
byId.set(task.id, row);
|
|
75
|
+
if (shown) rows.push(row);
|
|
76
|
+
if (!kids.length) {
|
|
77
|
+
row.span = spanOf(task);
|
|
78
|
+
row.progress = clampProgress(task.progress);
|
|
79
|
+
return row;
|
|
80
|
+
}
|
|
81
|
+
let start = Infinity;
|
|
82
|
+
let end = -Infinity;
|
|
83
|
+
let dateOnly = true;
|
|
84
|
+
let weighted = 0;
|
|
85
|
+
let weight = 0;
|
|
86
|
+
for (const child of kids) {
|
|
87
|
+
const sub = visit(child, depth + 1, shown && !task.collapsed);
|
|
88
|
+
if (!sub.span) continue;
|
|
89
|
+
start = Math.min(start, sub.span.start);
|
|
90
|
+
end = Math.max(end, sub.span.end);
|
|
91
|
+
dateOnly &&= sub.span.dateOnly;
|
|
92
|
+
if (sub.milestone) continue;
|
|
93
|
+
const length = sub.span.end - sub.span.start;
|
|
94
|
+
weighted += sub.progress * length;
|
|
95
|
+
weight += length;
|
|
96
|
+
}
|
|
97
|
+
if (start <= end) row.span = {
|
|
98
|
+
start,
|
|
99
|
+
end,
|
|
100
|
+
dateOnly
|
|
101
|
+
};
|
|
102
|
+
row.progress = weight ? weighted / weight : 0;
|
|
103
|
+
return row;
|
|
104
|
+
};
|
|
105
|
+
for (const task of childrenOf.get(void 0) ?? []) visit(task, 0, true);
|
|
106
|
+
return {
|
|
107
|
+
rows,
|
|
108
|
+
byId,
|
|
109
|
+
childrenOf
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export { buildTree };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","names":[],"sources":["../../src/core/tree.ts"],"sourcesContent":["import { clampProgress, spanOf, type GanttTask, type TaskSpan } from './model'\n\nexport interface GanttRow {\n task: GanttTask\n /** Nesting level, from 0. */\n depth: number\n /** Position among the shown rows; `-1` when a collapsed parent hides it. */\n index: number\n /** Number of direct children. */\n children: number\n /** Has children, so its dates and progress come from them. */\n summary: boolean\n milestone: boolean\n /** Its own dates, or for a summary the dates its children cover. */\n span: TaskSpan | null\n progress: number\n}\n\nexport interface GanttTree {\n /** The rows to show, top to bottom. */\n rows: GanttRow[]\n /** Every task's row, shown or not. */\n byId: Map<string, GanttRow>\n /** Children in display order; roots under `undefined`. */\n childrenOf: Map<string | undefined, GanttTask[]>\n}\n\n/** Between two siblings given the same `order`: by id. */\nconst sameOrder = (a: GanttTask, b: GanttTask) =>\n a.order == null || b.order == null || a.id === b.id ? 0 : a.id < b.id ? -1 : 1\n\n/**\n * Orders siblings: by `order` where given, otherwise as in the array. A\n * sibling without `order` sorts right after the one before it in the array.\n * Two siblings given the same `order` (two people inserting at the same\n * place) sort by id, so every client shows them alike.\n */\nfunction sortSiblings(list: { task: GanttTask; index: number }[]) {\n let key = ''\n const keyed = list.map((item) => {\n if (item.task.order != null) key = item.task.order\n return { ...item, key }\n })\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh array\n keyed.sort((a, b) =>\n a.key < b.key ? -1 : a.key > b.key ? 1 : sameOrder(a.task, b.task) || a.index - b.index,\n )\n return keyed.map((item) => item.task)\n}\n\n/**\n * Lays the flat task list out as a tree: rows in display order, summaries\n * worked out from their children. A `parent` that doesn't exist, or that\n * would make a loop, is ignored and the task becomes a root.\n */\nexport function buildTree(tasks: readonly GanttTask[]): GanttTree {\n const ids = new Map(tasks.map((task) => [task.id, task]))\n\n // follow each parent chain; a chain that comes back to itself is cut\n const parentOf = new Map<string, string | undefined>()\n for (const task of tasks) {\n let parent = task.parent != null && ids.has(task.parent) ? task.parent : undefined\n const seen = new Set([task.id])\n for (let up = parent; up != null;) {\n if (seen.has(up)) {\n parent = undefined\n break\n }\n seen.add(up)\n const next = ids.get(up)!.parent\n up = next != null && ids.has(next) ? next : undefined\n }\n parentOf.set(task.id, parent)\n }\n\n const groups = new Map<string | undefined, { task: GanttTask; index: number }[]>()\n tasks.forEach((task, index) => {\n const parent = parentOf.get(task.id)\n const list = groups.get(parent)\n if (list) list.push({ task, index })\n else groups.set(parent, [{ task, index }])\n })\n const childrenOf = new Map<string | undefined, GanttTask[]>()\n for (const [parent, list] of groups) childrenOf.set(parent, sortSiblings(list))\n\n const byId = new Map<string, GanttRow>()\n const rows: GanttRow[] = []\n\n const visit = (task: GanttTask, depth: number, shown: boolean): GanttRow => {\n const kids = childrenOf.get(task.id) ?? []\n const row: GanttRow = {\n task,\n depth,\n index: shown ? rows.length : -1,\n children: kids.length,\n summary: kids.length > 0,\n milestone: kids.length === 0 && task.type === 'milestone',\n span: null,\n progress: 0,\n }\n byId.set(task.id, row)\n if (shown) rows.push(row)\n\n if (!kids.length) {\n row.span = spanOf(task)\n row.progress = clampProgress(task.progress)\n return row\n }\n let start = Infinity\n let end = -Infinity\n let dateOnly = true\n let weighted = 0\n let weight = 0\n for (const child of kids) {\n const sub = visit(child, depth + 1, shown && !task.collapsed)\n if (!sub.span) continue\n start = Math.min(start, sub.span.start)\n end = Math.max(end, sub.span.end)\n dateOnly &&= sub.span.dateOnly\n if (sub.milestone) continue\n const length = sub.span.end - sub.span.start\n weighted += sub.progress * length\n weight += length\n }\n if (start <= end) row.span = { start, end, dateOnly }\n row.progress = weight ? weighted / weight : 0\n return row\n }\n\n for (const task of childrenOf.get(undefined) ?? []) visit(task, 0, true)\n return { rows, byId, childrenOf }\n}\n"],"mappings":"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"}
|