@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,231 @@
|
|
|
1
|
+
import { resolveGanttExtensions } from "./extension.js";
|
|
2
|
+
import { parseTime } from "./time.js";
|
|
3
|
+
import { linkRefusal, parseLag } from "./links.js";
|
|
4
|
+
// src/core/document.ts
|
|
5
|
+
/**
|
|
6
|
+
* What is wrong with the shape itself: the document is not an object, or a
|
|
7
|
+
* list in it is not an array. Nothing else can be checked until it is fixed.
|
|
8
|
+
*/
|
|
9
|
+
function shapeProblems(value, lists) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [{
|
|
11
|
+
code: "shape",
|
|
12
|
+
path: "",
|
|
13
|
+
text: "",
|
|
14
|
+
message: "The document is not an object"
|
|
15
|
+
}];
|
|
16
|
+
return lists.filter((key) => !Array.isArray(value[key])).map((key) => ({
|
|
17
|
+
code: "shape",
|
|
18
|
+
path: key,
|
|
19
|
+
text: "",
|
|
20
|
+
message: `"${key}" is not a list`
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The problems of a Gantt document, before it is saved or shown: an
|
|
25
|
+
* unknown version, tasks and links without an id or sharing one, `parent`
|
|
26
|
+
* pointing nowhere or in a circle, dates and lags that can't be read, and
|
|
27
|
+
* links to missing tasks, to the task itself, between a summary and its
|
|
28
|
+
* own task, twice between the same tasks, or in a circle; with `canLink`
|
|
29
|
+
* or `extensions`, links their rules refuse too.
|
|
30
|
+
*/
|
|
31
|
+
function checkDocument(document, options = {}) {
|
|
32
|
+
const shape = shapeProblems(document, ["tasks", "links"]);
|
|
33
|
+
if (shape.length) return shape;
|
|
34
|
+
const problems = [];
|
|
35
|
+
if (document.version !== 1) problems.push({
|
|
36
|
+
code: "version",
|
|
37
|
+
path: "version",
|
|
38
|
+
text: String(document.version),
|
|
39
|
+
message: `Version ${String(document.version)} is not known`
|
|
40
|
+
});
|
|
41
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
42
|
+
document.tasks.forEach((task, i) => {
|
|
43
|
+
const path = `tasks[${i}]`;
|
|
44
|
+
if (!task.id) {
|
|
45
|
+
problems.push({
|
|
46
|
+
code: "missing-id",
|
|
47
|
+
path,
|
|
48
|
+
text: "",
|
|
49
|
+
message: `${path} has no id`
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (tasks.has(task.id)) problems.push({
|
|
54
|
+
code: "duplicate-id",
|
|
55
|
+
taskId: task.id,
|
|
56
|
+
path,
|
|
57
|
+
text: task.id,
|
|
58
|
+
message: `The task id "${task.id}" is used more than once`
|
|
59
|
+
});
|
|
60
|
+
else tasks.set(task.id, task);
|
|
61
|
+
for (const key of ["start", "end"]) {
|
|
62
|
+
const text = task[key];
|
|
63
|
+
if (text == null || text === "" || parseTime(text)) continue;
|
|
64
|
+
problems.push({
|
|
65
|
+
code: "bad-date",
|
|
66
|
+
taskId: task.id,
|
|
67
|
+
path: `${path}.${key}`,
|
|
68
|
+
text: String(text),
|
|
69
|
+
message: `The ${key} "${String(text)}" of "${task.id}" is not a date`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
document.tasks.forEach((task, i) => {
|
|
74
|
+
if (!task.id || !task.parent || tasks.get(task.id) !== task) return;
|
|
75
|
+
const path = `tasks[${i}].parent`;
|
|
76
|
+
if (!tasks.has(task.parent)) {
|
|
77
|
+
problems.push({
|
|
78
|
+
code: "missing-parent",
|
|
79
|
+
taskId: task.id,
|
|
80
|
+
path,
|
|
81
|
+
text: task.parent,
|
|
82
|
+
message: `The parent "${task.parent}" of "${task.id}" is not in the document`
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
for (let at = tasks.get(task.parent); at; at = at.parent ? tasks.get(at.parent) : void 0) {
|
|
88
|
+
if (at.id !== task.id) {
|
|
89
|
+
if (seen.has(at.id)) break;
|
|
90
|
+
seen.add(at.id);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
problems.push({
|
|
94
|
+
code: "parent-cycle",
|
|
95
|
+
taskId: task.id,
|
|
96
|
+
path,
|
|
97
|
+
text: task.parent,
|
|
98
|
+
message: `The task "${task.id}" is inside itself through its parents`
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const rules = [...options.canLink ? [options.canLink] : [], ...options.extensions ? resolveGanttExtensions(options.extensions).linkRules : []];
|
|
104
|
+
const ids = /* @__PURE__ */ new Set();
|
|
105
|
+
/** The links before this one that join tasks that are there, for the rules between links. */
|
|
106
|
+
const sound = [];
|
|
107
|
+
document.links.forEach((link, i) => {
|
|
108
|
+
const path = `links[${i}]`;
|
|
109
|
+
if (!link.id) problems.push({
|
|
110
|
+
code: "missing-id",
|
|
111
|
+
path,
|
|
112
|
+
text: "",
|
|
113
|
+
message: `${path} has no id`
|
|
114
|
+
});
|
|
115
|
+
else if (ids.has(link.id)) problems.push({
|
|
116
|
+
code: "duplicate-id",
|
|
117
|
+
linkId: link.id,
|
|
118
|
+
path,
|
|
119
|
+
text: link.id,
|
|
120
|
+
message: `The link id "${link.id}" is used more than once`
|
|
121
|
+
});
|
|
122
|
+
ids.add(link.id);
|
|
123
|
+
if (!parseLag(link.lag)) problems.push({
|
|
124
|
+
code: "bad-lag",
|
|
125
|
+
linkId: link.id,
|
|
126
|
+
path: `${path}.lag`,
|
|
127
|
+
text: String(link.lag),
|
|
128
|
+
message: `The lag "${String(link.lag)}" of "${link.id}" can't be read`
|
|
129
|
+
});
|
|
130
|
+
let missing = false;
|
|
131
|
+
for (const end of ["from", "to"]) {
|
|
132
|
+
if (tasks.has(link[end])) continue;
|
|
133
|
+
missing = true;
|
|
134
|
+
problems.push({
|
|
135
|
+
code: "missing-task",
|
|
136
|
+
linkId: link.id,
|
|
137
|
+
taskId: link[end] || void 0,
|
|
138
|
+
path: `${path}.${end}`,
|
|
139
|
+
text: String(link[end] ?? ""),
|
|
140
|
+
message: `The task "${String(link[end])}" the link "${link.id}" points at is not in the document`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (missing) return;
|
|
144
|
+
const refusal = linkRefusal(tasks, sound, link.from, link.to);
|
|
145
|
+
if (!refusal) {
|
|
146
|
+
sound.push(link);
|
|
147
|
+
for (const rule of rules) {
|
|
148
|
+
const answer = rule(link, document);
|
|
149
|
+
if (answer === true) continue;
|
|
150
|
+
problems.push({
|
|
151
|
+
code: "refused-link",
|
|
152
|
+
linkId: link.id,
|
|
153
|
+
path,
|
|
154
|
+
text: typeof answer === "string" ? answer : "",
|
|
155
|
+
message: `The link "${link.id}" is refused by a canLink rule${typeof answer === "string" && answer ? `: ${answer}` : ""}`
|
|
156
|
+
});
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
problems.push({
|
|
162
|
+
code: LINK_PROBLEMS[refusal][0],
|
|
163
|
+
linkId: link.id,
|
|
164
|
+
path,
|
|
165
|
+
text: link.id,
|
|
166
|
+
message: LINK_PROBLEMS[refusal][1](link)
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return problems;
|
|
170
|
+
}
|
|
171
|
+
/** What each refusal of `linkRefusal` is as a problem; `custom` can't come without rules. */
|
|
172
|
+
var LINK_PROBLEMS = {
|
|
173
|
+
self: ["self-link", (link) => `The link "${link.id}" joins "${link.from}" to itself`],
|
|
174
|
+
nested: ["nested-link", (link) => `The link "${link.id}" joins a summary task and a task inside it ("${link.from}", "${link.to}")`],
|
|
175
|
+
duplicate: ["duplicate-link", (link) => `The link "${link.id}" joins "${link.from}" and "${link.to}", which another link already joins`],
|
|
176
|
+
cycle: ["link-cycle", (link) => `The link "${link.id}" from "${link.from}" to "${link.to}" closes a circle of links`]
|
|
177
|
+
};
|
|
178
|
+
var objects = (value) => (Array.isArray(value) ? value : []).filter((item) => !!item && typeof item === "object");
|
|
179
|
+
/** The saved data as an object of version 1, or an error saying why it can't be. */
|
|
180
|
+
function sourceOf(input) {
|
|
181
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`[@dou-ui/gantt] a Gantt document must be an object, not ${Array.isArray(input) ? "an array" : JSON.stringify(input) ?? String(input)}`);
|
|
182
|
+
const source = input;
|
|
183
|
+
const version = source.version ?? 1;
|
|
184
|
+
if (version !== 1) throw new Error(`[@dou-ui/gantt] unknown document version ${JSON.stringify(version) ?? String(version)} (expected 1); data saved by a newer @dou-ui/gantt needs that version to open`);
|
|
185
|
+
return source;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Brings a saved document up to the current version, and fills what an
|
|
189
|
+
* older or hand-written one may lack (`version`, `tasks`, `links`). Tasks
|
|
190
|
+
* and links that are not objects are dropped; anything else is kept as it
|
|
191
|
+
* is. Throws for data that is not an object
|
|
192
|
+
* (an array, `null`, a string) and for a version it does not know.
|
|
193
|
+
*/
|
|
194
|
+
function migrateDocument(input) {
|
|
195
|
+
const source = sourceOf(input);
|
|
196
|
+
return {
|
|
197
|
+
...source,
|
|
198
|
+
version: 1,
|
|
199
|
+
tasks: objects(source.tasks),
|
|
200
|
+
links: objects(source.links)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
var warned = /* @__PURE__ */ new Set();
|
|
204
|
+
/**
|
|
205
|
+
* The `v-model` as the chart reads it. A document of the wrong shape (not an
|
|
206
|
+
* object, without `tasks` or `links`, without a known `version`) gets a
|
|
207
|
+
* warning in development and is read with what is missing left empty and the
|
|
208
|
+
* version as 1, so the chart still opens and what it writes back carries
|
|
209
|
+
* `version: 1`. The scheduler doesn't use `links` and doesn't need them.
|
|
210
|
+
*/
|
|
211
|
+
function readDocument(value, needsLinks = true) {
|
|
212
|
+
const plain = !!value && typeof value === "object" && !Array.isArray(value);
|
|
213
|
+
const source = plain ? value : {};
|
|
214
|
+
const tasks = Array.isArray(source.tasks);
|
|
215
|
+
const links = Array.isArray(source.links);
|
|
216
|
+
const problem = !plain ? `is ${Array.isArray(value) ? "an array" : JSON.stringify(value) ?? String(value)}` : !tasks ? "has no tasks array" : !links && needsLinks ? "has no links array" : source.version === void 0 ? "has no version" : source.version !== 1 ? `has version ${JSON.stringify(source.version)}` : "";
|
|
217
|
+
if (!problem && links) return value;
|
|
218
|
+
if (problem && process.env.NODE_ENV !== "production" && !warned.has(problem)) {
|
|
219
|
+
warned.add(problem);
|
|
220
|
+
console.warn(`[@dou-ui/gantt] the v-model ${problem}; expected { version: 1, tasks: [...], links: [...] }. What is missing is read as empty and the version as 1; read saved data with migrateDocument().`);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
...source,
|
|
224
|
+
version: 1,
|
|
225
|
+
tasks: tasks ? source.tasks : [],
|
|
226
|
+
links: links ? source.links : []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export { checkDocument, migrateDocument, readDocument };
|
|
230
|
+
|
|
231
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","names":[],"sources":["../../src/core/document.ts"],"sourcesContent":["import { resolveGanttExtensions, type GanttExtension } from './extension'\nimport { linkRefusal, parseLag } from './links'\nimport type { GanttLink, GanttTask } from './model'\nimport { parseTime } from './time'\nimport type { GanttDocument } from './transaction'\n\nexport interface DocumentProblem {\n code:\n | 'version'\n | 'shape'\n | 'missing-id'\n | 'duplicate-id'\n | 'missing-parent'\n | 'parent-cycle'\n | 'bad-date'\n | 'missing-task'\n | 'bad-lag'\n | 'self-link'\n | 'nested-link'\n | 'duplicate-link'\n | 'link-cycle'\n | 'refused-link'\n /** The task at fault, or the task a link points at. */\n taskId?: string\n /** The link at fault. */\n linkId?: string\n /**\n * Where the problem is, as a path into the document, e.g. `tasks[2].start`\n * or `links[0].to`; `version` for the version.\n */\n path: string\n /** The id, date or lag at fault, for the editor's message. */\n text: string\n /** An English description, for logs. */\n message: string\n}\n\n/**\n * What is wrong with the shape itself: the document is not an object, or a\n * list in it is not an array. Nothing else can be checked until it is fixed.\n */\nfunction shapeProblems(value: unknown, lists: string[]): DocumentProblem[] {\n if (!value || typeof value !== 'object' || Array.isArray(value))\n return [{ code: 'shape', path: '', text: '', message: 'The document is not an object' }]\n return lists\n .filter((key) => !Array.isArray((value as Record<string, unknown>)[key]))\n .map((key) => ({ code: 'shape', path: key, text: '', message: `\"${key}\" is not a list` }))\n}\n\nexport interface CheckDocumentOptions {\n /** The chart's `canLink`, to check the links against it too. */\n canLink?: GanttRule\n /** The chart's extensions, to check the links against their `canLink` too. */\n extensions?: readonly (GanttExtension | readonly GanttExtension[])[]\n}\n\ntype GanttRule = NonNullable<GanttExtension['canLink']>\n\n/**\n * The problems of a Gantt document, before it is saved or shown: an\n * unknown version, tasks and links without an id or sharing one, `parent`\n * pointing nowhere or in a circle, dates and lags that can't be read, and\n * links to missing tasks, to the task itself, between a summary and its\n * own task, twice between the same tasks, or in a circle; with `canLink`\n * or `extensions`, links their rules refuse too.\n */\nexport function checkDocument(\n document: GanttDocument,\n options: CheckDocumentOptions = {},\n): DocumentProblem[] {\n const shape = shapeProblems(document, ['tasks', 'links'])\n if (shape.length) return shape\n const problems: DocumentProblem[] = []\n if (document.version !== 1)\n problems.push({\n code: 'version',\n path: 'version',\n text: String(document.version),\n message: `Version ${String(document.version)} is not known`,\n })\n\n const tasks = new Map<string, GanttTask>()\n document.tasks.forEach((task, i) => {\n const path = `tasks[${i}]`\n if (!task.id) {\n problems.push({ code: 'missing-id', path, text: '', message: `${path} has no id` })\n return\n }\n if (tasks.has(task.id))\n problems.push({\n code: 'duplicate-id',\n taskId: task.id,\n path,\n text: task.id,\n message: `The task id \"${task.id}\" is used more than once`,\n })\n else tasks.set(task.id, task)\n for (const key of ['start', 'end'] as const) {\n const text = task[key]\n if (text == null || text === '' || parseTime(text)) continue\n problems.push({\n code: 'bad-date',\n taskId: task.id,\n path: `${path}.${key}`,\n text: String(text),\n message: `The ${key} \"${String(text)}\" of \"${task.id}\" is not a date`,\n })\n }\n })\n\n document.tasks.forEach((task, i) => {\n if (!task.id || !task.parent || tasks.get(task.id) !== task) return\n const path = `tasks[${i}].parent`\n if (!tasks.has(task.parent)) {\n problems.push({\n code: 'missing-parent',\n taskId: task.id,\n path,\n text: task.parent,\n message: `The parent \"${task.parent}\" of \"${task.id}\" is not in the document`,\n })\n return\n }\n // only the members of a circle are reported; one hanging below it is fine itself\n const seen = new Set<string>()\n for (let at = tasks.get(task.parent); at; at = at.parent ? tasks.get(at.parent) : undefined) {\n if (at.id !== task.id) {\n if (seen.has(at.id)) break\n seen.add(at.id)\n continue\n }\n problems.push({\n code: 'parent-cycle',\n taskId: task.id,\n path,\n text: task.parent,\n message: `The task \"${task.id}\" is inside itself through its parents`,\n })\n break\n }\n })\n\n const rules: GanttRule[] = [\n ...(options.canLink ? [options.canLink] : []),\n ...(options.extensions ? resolveGanttExtensions(options.extensions).linkRules : []),\n ]\n const ids = new Set<string>()\n /** The links before this one that join tasks that are there, for the rules between links. */\n const sound: GanttLink[] = []\n document.links.forEach((link, i) => {\n const path = `links[${i}]`\n if (!link.id)\n problems.push({ code: 'missing-id', path, text: '', message: `${path} has no id` })\n else if (ids.has(link.id))\n problems.push({\n code: 'duplicate-id',\n linkId: link.id,\n path,\n text: link.id,\n message: `The link id \"${link.id}\" is used more than once`,\n })\n ids.add(link.id)\n if (!parseLag(link.lag))\n problems.push({\n code: 'bad-lag',\n linkId: link.id,\n path: `${path}.lag`,\n text: String(link.lag),\n message: `The lag \"${String(link.lag)}\" of \"${link.id}\" can't be read`,\n })\n let missing = false\n for (const end of ['from', 'to'] as const) {\n if (tasks.has(link[end])) continue\n missing = true\n problems.push({\n code: 'missing-task',\n linkId: link.id,\n taskId: link[end] || undefined,\n path: `${path}.${end}`,\n text: String(link[end] ?? ''),\n message: `The task \"${String(link[end])}\" the link \"${link.id}\" points at is not in the document`,\n })\n }\n if (missing) return\n const refusal = linkRefusal(tasks, sound, link.from, link.to)\n if (!refusal) {\n sound.push(link)\n for (const rule of rules) {\n const answer = rule(link, document)\n if (answer === true) continue\n problems.push({\n code: 'refused-link',\n linkId: link.id,\n path,\n text: typeof answer === 'string' ? answer : '',\n message: `The link \"${link.id}\" is refused by a canLink rule${typeof answer === 'string' && answer ? `: ${answer}` : ''}`,\n })\n break\n }\n return\n }\n problems.push({\n code: LINK_PROBLEMS[refusal as keyof typeof LINK_PROBLEMS][0],\n linkId: link.id,\n path,\n text: link.id,\n message: LINK_PROBLEMS[refusal as keyof typeof LINK_PROBLEMS][1](link),\n })\n })\n return problems\n}\n\n/** What each refusal of `linkRefusal` is as a problem; `custom` can't come without rules. */\nconst LINK_PROBLEMS = {\n self: ['self-link', (link: GanttLink) => `The link \"${link.id}\" joins \"${link.from}\" to itself`],\n nested: [\n 'nested-link',\n (link: GanttLink) =>\n `The link \"${link.id}\" joins a summary task and a task inside it (\"${link.from}\", \"${link.to}\")`,\n ],\n duplicate: [\n 'duplicate-link',\n (link: GanttLink) =>\n `The link \"${link.id}\" joins \"${link.from}\" and \"${link.to}\", which another link already joins`,\n ],\n cycle: [\n 'link-cycle',\n (link: GanttLink) =>\n `The link \"${link.id}\" from \"${link.from}\" to \"${link.to}\" closes a circle of links`,\n ],\n} as const satisfies Record<string, readonly [DocumentProblem['code'], (link: GanttLink) => string]>\n\nconst objects = <T>(value: unknown) =>\n (Array.isArray(value) ? value : []).filter(\n (item): item is T => !!item && typeof item === 'object',\n )\n\n/** The saved data as an object of version 1, or an error saying why it can't be. */\nfunction sourceOf(input: unknown): Record<string, unknown> {\n if (!input || typeof input !== 'object' || Array.isArray(input))\n throw new Error(\n `[@dou-ui/gantt] a Gantt document must be an object, not ${Array.isArray(input) ? 'an array' : (JSON.stringify(input) ?? String(input))}`,\n )\n const source = input as Record<string, unknown>\n const version = source.version ?? 1\n if (version !== 1)\n throw new Error(\n `[@dou-ui/gantt] unknown document version ${JSON.stringify(version) ?? String(version)} (expected 1); data saved by a newer @dou-ui/gantt needs that version to open`,\n )\n return source\n}\n\n/**\n * Brings a saved document up to the current version, and fills what an\n * older or hand-written one may lack (`version`, `tasks`, `links`). Tasks\n * and links that are not objects are dropped; anything else is kept as it\n * is. Throws for data that is not an object\n * (an array, `null`, a string) and for a version it does not know.\n */\nexport function migrateDocument(input: unknown): GanttDocument {\n const source = sourceOf(input)\n return {\n ...source,\n version: 1,\n tasks: objects<GanttTask>(source.tasks),\n links: objects<GanttLink>(source.links),\n }\n}\n\nconst warned = new Set<string>()\n\n/**\n * The `v-model` as the chart reads it. A document of the wrong shape (not an\n * object, without `tasks` or `links`, without a known `version`) gets a\n * warning in development and is read with what is missing left empty and the\n * version as 1, so the chart still opens and what it writes back carries\n * `version: 1`. The scheduler doesn't use `links` and doesn't need them.\n */\nexport function readDocument(value: unknown, needsLinks = true): GanttDocument {\n const plain = !!value && typeof value === 'object' && !Array.isArray(value)\n const source = (plain ? value : {}) as Partial<GanttDocument>\n const tasks = Array.isArray(source.tasks)\n const links = Array.isArray(source.links)\n const problem = !plain\n ? `is ${Array.isArray(value) ? 'an array' : (JSON.stringify(value) ?? String(value))}`\n : !tasks\n ? 'has no tasks array'\n : !links && needsLinks\n ? 'has no links array'\n : source.version === undefined\n ? 'has no version'\n : source.version !== 1\n ? `has version ${JSON.stringify(source.version)}`\n : ''\n if (!problem && links) return value as GanttDocument\n if (problem && process.env.NODE_ENV !== 'production' && !warned.has(problem)) {\n warned.add(problem)\n console.warn(\n `[@dou-ui/gantt] the v-model ${problem}; expected { version: 1, tasks: [...], links: [...] }. What is missing is read as empty and the version as 1; read saved data with migrateDocument().`,\n )\n }\n return {\n ...source,\n version: 1,\n tasks: tasks ? source.tasks! : [],\n links: links ? source.links! : [],\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/core/extension.ts
|
|
2
|
+
/** Declares an extension; only gives it a type. */
|
|
3
|
+
function defineGanttExtension(extension) {
|
|
4
|
+
return extension;
|
|
5
|
+
}
|
|
6
|
+
function flatten(list) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const item of list) if (Array.isArray(item)) out.push(...flatten(item));
|
|
9
|
+
else out.push(item);
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
/** Sorts the extensions by priority and collects what they add. */
|
|
13
|
+
function resolveGanttExtensions(list = []) {
|
|
14
|
+
const extensions = flatten(list).map((extension, index) => ({
|
|
15
|
+
extension,
|
|
16
|
+
index
|
|
17
|
+
})).sort((a, b) => (b.extension.priority ?? 100) - (a.extension.priority ?? 100) || a.index - b.index).map((entry) => entry.extension);
|
|
18
|
+
const names = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const extension of extensions) {
|
|
20
|
+
if (names.has(extension.name)) throw new Error(`[@dou-ui/gantt] the extension "${extension.name}" is added twice`);
|
|
21
|
+
names.add(extension.name);
|
|
22
|
+
}
|
|
23
|
+
const pick = (key) => extensions.flatMap((e) => e[key] ? [e[key]] : []);
|
|
24
|
+
return {
|
|
25
|
+
extensions,
|
|
26
|
+
columns: extensions.flatMap((e) => e.columns ?? []),
|
|
27
|
+
linkRules: pick("canLink"),
|
|
28
|
+
moveRules: pick("canMove"),
|
|
29
|
+
keymaps: pick("keymap"),
|
|
30
|
+
markers: pick("markers"),
|
|
31
|
+
menus: pick("contextMenu"),
|
|
32
|
+
setups: pick("setup"),
|
|
33
|
+
transactions: pick("transaction"),
|
|
34
|
+
history: extensions.find((e) => e.history)?.history,
|
|
35
|
+
below: extensions.flatMap((e) => e.layers?.below ?? []),
|
|
36
|
+
above: extensions.flatMap((e) => e.layers?.above ?? []),
|
|
37
|
+
overlays: extensions.flatMap((e) => e.overlays ?? [])
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export { defineGanttExtension, resolveGanttExtensions };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=extension.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.js","names":[],"sources":["../../src/core/extension.ts"],"sourcesContent":["import type { ContextMenuItem } from 'dou-ui'\nimport type { Component } from 'vue'\nimport type { GanttContext } from '../context'\nimport type { GanttColumn, GanttMarker, GanttMove } from '../types'\nimport type { GanttMenuTarget } from './menu'\nimport type { GanttLink } from './model'\nimport type { GanttDocument, GanttTransaction } from './transaction'\n\n/** What an extension's hooks receive; the same as `useGantt()`. */\nexport type GanttExtensionContext = GanttContext\n\n/**\n * A key handler: return `false` to let the key through to the next handler\n * and the chart's own keys; anything else means it was handled.\n */\nexport type GanttKeyHandler = (event: KeyboardEvent) => boolean | void\n\n/** An undo history that replaces the chart's own, e.g. one that only undoes this user's changes. */\nexport interface GanttHistoryHandler {\n /** `false` when there is nothing to undo. */\n undo: () => boolean\n redo: () => boolean\n canUndo: () => boolean\n canRedo: () => boolean\n}\n\n/**\n * A piece of the Gantt chart: table columns, rules for links and moves,\n * keys, marker lines and extra layers. A third-party extension can do\n * anything the chart's own props can.\n */\nexport interface GanttExtension {\n name: string\n /** Higher runs first: its keys and rules are asked before others. Defaults to 100. */\n priority?: number\n /** Columns added after the table's own. */\n columns?: GanttColumn[]\n /** A link rule, asked with `can-link`. Return `true` to allow, or a sentence saying why not. */\n canLink?: (link: Pick<GanttLink, 'from' | 'to' | 'type'>, doc: GanttDocument) => boolean | string\n /** A rule for new dates, asked with `can-move`. */\n canMove?: (move: GanttMove, doc: GanttDocument) => boolean | string\n /** Keys, written as in `useHotkey` (e.g. `mod+d`, `shift+t`), while the chart has focus. */\n keymap?: (context: GanttExtensionContext) => Record<string, GanttKeyHandler>\n /**\n * Items for the context menu, which opens on a right click, the menu key\n * or a long press. They come before the built-in ones.\n */\n contextMenu?: (context: GanttExtensionContext, target: GanttMenuTarget) => ContextMenuItem[]\n /** Vertical marker lines, such as a release day; worked out again when what they read changes. */\n markers?: (context: GanttExtensionContext) => GanttMarker[]\n /**\n * Components drawn on the timeline, in its own coordinates: `below` under\n * the dependency lines, `above` over the bars. They scroll with it.\n */\n layers?: { below?: Component[]; above?: Component[] }\n /** Components drawn over the chart, not scrolled, e.g. a panel. They can call `useGantt()`. */\n overlays?: Component[]\n /** Runs once the chart is mounted. Return a function to run when it unmounts. */\n setup?: (context: GanttExtensionContext) => void | (() => void)\n /**\n * Sees every change before it reaches the tasks and links, in priority\n * order, and may add steps to it. Undo and redo come through here too.\n */\n transaction?: (tr: GanttTransaction, context: GanttExtensionContext) => void\n /** Replaces the chart's undo history; the first extension that has one wins. */\n history?: (context: GanttExtensionContext) => GanttHistoryHandler\n}\n\n/** Declares an extension; only gives it a type. */\nexport function defineGanttExtension(extension: GanttExtension): GanttExtension {\n return extension\n}\n\n/** Everything the extensions contribute, gathered once per list. */\nexport interface ResolvedGanttExtensions {\n extensions: GanttExtension[]\n columns: GanttColumn[]\n linkRules: NonNullable<GanttExtension['canLink']>[]\n moveRules: NonNullable<GanttExtension['canMove']>[]\n keymaps: NonNullable<GanttExtension['keymap']>[]\n markers: NonNullable<GanttExtension['markers']>[]\n menus: NonNullable<GanttExtension['contextMenu']>[]\n setups: NonNullable<GanttExtension['setup']>[]\n transactions: NonNullable<GanttExtension['transaction']>[]\n history: GanttExtension['history']\n below: Component[]\n above: Component[]\n overlays: Component[]\n}\n\ntype ExtensionList = readonly (GanttExtension | readonly GanttExtension[])[]\n\nfunction flatten(list: ExtensionList): GanttExtension[] {\n const out: GanttExtension[] = []\n for (const item of list) {\n if (Array.isArray(item)) out.push(...flatten(item))\n else out.push(item as GanttExtension)\n }\n return out\n}\n\n/** Sorts the extensions by priority and collects what they add. */\nexport function resolveGanttExtensions(list: ExtensionList = []): ResolvedGanttExtensions {\n const extensions = flatten(list)\n .map((extension, index) => ({ extension, index }))\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh array\n .sort(\n (a, b) => (b.extension.priority ?? 100) - (a.extension.priority ?? 100) || a.index - b.index,\n )\n .map((entry) => entry.extension)\n\n const names = new Set<string>()\n for (const extension of extensions) {\n if (names.has(extension.name))\n throw new Error(`[@dou-ui/gantt] the extension \"${extension.name}\" is added twice`)\n names.add(extension.name)\n }\n\n const pick = <K extends keyof GanttExtension>(key: K) =>\n extensions.flatMap((e) => (e[key] ? [e[key]] : [])) as NonNullable<GanttExtension[K]>[]\n\n return {\n extensions,\n columns: extensions.flatMap((e) => e.columns ?? []),\n linkRules: pick('canLink'),\n moveRules: pick('canMove'),\n keymaps: pick('keymap'),\n markers: pick('markers'),\n menus: pick('contextMenu'),\n setups: pick('setup'),\n transactions: pick('transaction'),\n history: extensions.find((e) => e.history)?.history,\n below: extensions.flatMap((e) => e.layers?.below ?? []),\n above: extensions.flatMap((e) => e.layers?.above ?? []),\n overlays: extensions.flatMap((e) => e.overlays ?? []),\n }\n}\n"],"mappings":";;AAqEA,SAAgB,qBAAqB,WAA2C;CAC9E,OAAO;AACT;AAqBA,SAAS,QAAQ,MAAuC;CACtD,MAAM,MAAwB,CAAC;CAC/B,KAAK,MAAM,QAAQ,MACjB,IAAI,MAAM,QAAQ,IAAI,GAAG,IAAI,KAAK,GAAG,QAAQ,IAAI,CAAC;MAC7C,IAAI,KAAK,IAAsB;CAEtC,OAAO;AACT;;AAGA,SAAgB,uBAAuB,OAAsB,CAAC,GAA4B;CACxF,MAAM,aAAa,QAAQ,IAAI,CAAC,CAC7B,KAAK,WAAW,WAAW;EAAE;EAAW;CAAM,EAAE,CAAC,CAEjD,MACE,GAAG,OAAO,EAAE,UAAU,YAAY,QAAQ,EAAE,UAAU,YAAY,QAAQ,EAAE,QAAQ,EAAE,KACzF,CAAC,CACA,KAAK,UAAU,MAAM,SAAS;CAEjC,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,IAAI,MAAM,IAAI,UAAU,IAAI,GAC1B,MAAM,IAAI,MAAM,kCAAkC,UAAU,KAAK,iBAAiB;EACpF,MAAM,IAAI,UAAU,IAAI;CAC1B;CAEA,MAAM,QAAwC,QAC5C,WAAW,SAAS,MAAO,EAAE,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,CAAE;CAEpD,OAAO;EACL;EACA,SAAS,WAAW,SAAS,MAAM,EAAE,WAAW,CAAC,CAAC;EAClD,WAAW,KAAK,SAAS;EACzB,WAAW,KAAK,SAAS;EACzB,SAAS,KAAK,QAAQ;EACtB,SAAS,KAAK,SAAS;EACvB,OAAO,KAAK,aAAa;EACzB,QAAQ,KAAK,OAAO;EACpB,cAAc,KAAK,aAAa;EAChC,SAAS,WAAW,MAAM,MAAM,EAAE,OAAO,CAAC,EAAE;EAC5C,OAAO,WAAW,SAAS,MAAM,EAAE,QAAQ,SAAS,CAAC,CAAC;EACtD,OAAO,WAAW,SAAS,MAAM,EAAE,QAAQ,SAAS,CAAC,CAAC;EACtD,UAAU,WAAW,SAAS,MAAM,EAAE,YAAY,CAAC,CAAC;CACtD;AACF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/core/geometry.ts
|
|
2
|
+
/**
|
|
3
|
+
* A path through `points` with straight segments and corners rounded by
|
|
4
|
+
* up to `radius`; a corner never takes more than half of either segment.
|
|
5
|
+
*/
|
|
6
|
+
function roundedPath(points, radius = 4) {
|
|
7
|
+
const clean = [];
|
|
8
|
+
for (const point of points) {
|
|
9
|
+
const last = clean[clean.length - 1];
|
|
10
|
+
if (last && last.x === point.x && last.y === point.y) continue;
|
|
11
|
+
const before = clean[clean.length - 2];
|
|
12
|
+
if (before && last && (before.x === last.x && last.x === point.x || before.y === last.y && last.y === point.y)) clean.pop();
|
|
13
|
+
clean.push(point);
|
|
14
|
+
}
|
|
15
|
+
if (!clean.length) return "";
|
|
16
|
+
let d = `M${clean[0].x} ${clean[0].y}`;
|
|
17
|
+
for (let i = 1; i < clean.length - 1; i++) {
|
|
18
|
+
const prev = clean[i - 1];
|
|
19
|
+
const at = clean[i];
|
|
20
|
+
const next = clean[i + 1];
|
|
21
|
+
const into = Math.hypot(at.x - prev.x, at.y - prev.y);
|
|
22
|
+
const out = Math.hypot(next.x - at.x, next.y - at.y);
|
|
23
|
+
const r = Math.min(radius, into / 2, out / 2);
|
|
24
|
+
const a = {
|
|
25
|
+
x: at.x - (at.x - prev.x) / into * r,
|
|
26
|
+
y: at.y - (at.y - prev.y) / into * r
|
|
27
|
+
};
|
|
28
|
+
const b = {
|
|
29
|
+
x: at.x + (next.x - at.x) / out * r,
|
|
30
|
+
y: at.y + (next.y - at.y) / out * r
|
|
31
|
+
};
|
|
32
|
+
d += ` L${a.x} ${a.y} Q${at.x} ${at.y} ${b.x} ${b.y}`;
|
|
33
|
+
}
|
|
34
|
+
const last = clean[clean.length - 1];
|
|
35
|
+
return `${d} L${last.x} ${last.y}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The route of a dependency line: out of one end of `from`, into one end
|
|
39
|
+
* of `to`. It leaves and arrives horizontally, `gap` px clear of the bars,
|
|
40
|
+
* and takes a single vertical run when one fits; otherwise it turns back
|
|
41
|
+
* along the boundary between the rows (`rowHeight` apart).
|
|
42
|
+
*/
|
|
43
|
+
function routeLink(from, to, type = "fs", rowHeight = 36, gap = 12) {
|
|
44
|
+
const leavesEnd = type === "fs" || type === "ff";
|
|
45
|
+
const entersStart = type === "fs" || type === "ss";
|
|
46
|
+
const out = leavesEnd ? 1 : -1;
|
|
47
|
+
const into = entersStart ? 1 : -1;
|
|
48
|
+
const p = {
|
|
49
|
+
x: leavesEnd ? from.right : from.left,
|
|
50
|
+
y: from.y
|
|
51
|
+
};
|
|
52
|
+
const q = {
|
|
53
|
+
x: entersStart ? to.left : to.right,
|
|
54
|
+
y: to.y
|
|
55
|
+
};
|
|
56
|
+
const a = p.x + out * gap;
|
|
57
|
+
const b = q.x - into * gap;
|
|
58
|
+
const low = Math.max(out > 0 ? a : -Infinity, into < 0 ? b : -Infinity);
|
|
59
|
+
const high = Math.min(out < 0 ? a : Infinity, into > 0 ? b : Infinity);
|
|
60
|
+
let points;
|
|
61
|
+
if (low <= high || from.y === to.y) {
|
|
62
|
+
const x = out > 0 ? low : high;
|
|
63
|
+
points = [
|
|
64
|
+
p,
|
|
65
|
+
{
|
|
66
|
+
x,
|
|
67
|
+
y: p.y
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
x,
|
|
71
|
+
y: q.y
|
|
72
|
+
},
|
|
73
|
+
q
|
|
74
|
+
];
|
|
75
|
+
} else {
|
|
76
|
+
const mid = p.y + Math.sign(q.y - p.y) * (rowHeight / 2);
|
|
77
|
+
points = [
|
|
78
|
+
p,
|
|
79
|
+
{
|
|
80
|
+
x: a,
|
|
81
|
+
y: p.y
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
x: a,
|
|
85
|
+
y: mid
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
x: b,
|
|
89
|
+
y: mid
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
x: b,
|
|
93
|
+
y: q.y
|
|
94
|
+
},
|
|
95
|
+
q
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
points,
|
|
100
|
+
tip: q,
|
|
101
|
+
direction: into
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** A small solid arrowhead with its tip at `tip`. */
|
|
105
|
+
function arrowPath(tip, direction, size = 6) {
|
|
106
|
+
const back = tip.x - direction * size;
|
|
107
|
+
const half = size / 2 + .5;
|
|
108
|
+
return `M${tip.x} ${tip.y} L${back} ${tip.y - half} L${back} ${tip.y + half} Z`;
|
|
109
|
+
}
|
|
110
|
+
export { arrowPath, roundedPath, routeLink };
|
|
111
|
+
|
|
112
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry.js","names":[],"sources":["../../src/core/geometry.ts"],"sourcesContent":["import type { GanttLinkType } from './model'\n\nexport interface Point {\n x: number\n y: number\n}\n\n/**\n * A path through `points` with straight segments and corners rounded by\n * up to `radius`; a corner never takes more than half of either segment.\n */\nexport function roundedPath(points: readonly Point[], radius = 4) {\n // drop points that don't turn: repeats and ones on a straight line\n const clean: Point[] = []\n for (const point of points) {\n const last = clean[clean.length - 1]\n if (last && last.x === point.x && last.y === point.y) continue\n const before = clean[clean.length - 2]\n if (\n before &&\n last &&\n ((before.x === last.x && last.x === point.x) || (before.y === last.y && last.y === point.y))\n )\n clean.pop()\n clean.push(point)\n }\n if (!clean.length) return ''\n let d = `M${clean[0]!.x} ${clean[0]!.y}`\n for (let i = 1; i < clean.length - 1; i++) {\n const prev = clean[i - 1]!\n const at = clean[i]!\n const next = clean[i + 1]!\n const into = Math.hypot(at.x - prev.x, at.y - prev.y)\n const out = Math.hypot(next.x - at.x, next.y - at.y)\n const r = Math.min(radius, into / 2, out / 2)\n const a = { x: at.x - ((at.x - prev.x) / into) * r, y: at.y - ((at.y - prev.y) / into) * r }\n const b = { x: at.x + ((next.x - at.x) / out) * r, y: at.y + ((next.y - at.y) / out) * r }\n d += ` L${a.x} ${a.y} Q${at.x} ${at.y} ${b.x} ${b.y}`\n }\n const last = clean[clean.length - 1]!\n return `${d} L${last.x} ${last.y}`\n}\n\n/** Where a bar sits on screen, in px. */\nexport interface BarBox {\n /** Left and right edges; equal for a milestone's centre. */\n left: number\n right: number\n /** Vertical centre. */\n y: number\n}\n\nexport interface LinkRoute {\n points: Point[]\n /** The arrow's tip, and which way it points: `1` right, `-1` left. */\n tip: Point\n direction: 1 | -1\n}\n\n/**\n * The route of a dependency line: out of one end of `from`, into one end\n * of `to`. It leaves and arrives horizontally, `gap` px clear of the bars,\n * and takes a single vertical run when one fits; otherwise it turns back\n * along the boundary between the rows (`rowHeight` apart).\n */\nexport function routeLink(\n from: BarBox,\n to: BarBox,\n type: GanttLinkType = 'fs',\n rowHeight = 36,\n gap = 12,\n): LinkRoute {\n const leavesEnd = type === 'fs' || type === 'ff'\n const entersStart = type === 'fs' || type === 'ss'\n const out = leavesEnd ? 1 : -1\n const into = entersStart ? 1 : -1\n const p = { x: leavesEnd ? from.right : from.left, y: from.y }\n const q = { x: entersStart ? to.left : to.right, y: to.y }\n const a = p.x + out * gap\n const b = q.x - into * gap\n\n // one vertical run at x works when x is past `a` going out and before `b` coming in\n const low = Math.max(out > 0 ? a : -Infinity, into < 0 ? b : -Infinity)\n const high = Math.min(out < 0 ? a : Infinity, into > 0 ? b : Infinity)\n let points: Point[]\n if (low <= high || from.y === to.y) {\n const x = out > 0 ? low : high\n points = [p, { x, y: p.y }, { x, y: q.y }, q]\n } else {\n const mid = p.y + Math.sign(q.y - p.y) * (rowHeight / 2)\n points = [p, { x: a, y: p.y }, { x: a, y: mid }, { x: b, y: mid }, { x: b, y: q.y }, q]\n }\n return { points, tip: q, direction: into }\n}\n\n/** A small solid arrowhead with its tip at `tip`. */\nexport function arrowPath(tip: Point, direction: 1 | -1, size = 6) {\n const back = tip.x - direction * size\n const half = size / 2 + 0.5\n return `M${tip.x} ${tip.y} L${back} ${tip.y - half} L${back} ${tip.y + half} Z`\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { applyGanttSteps, compactSteps } from "./transaction.js";
|
|
2
|
+
// src/core/history.ts
|
|
3
|
+
/** Transactions in one group merge when they come within this many ms. */
|
|
4
|
+
var GROUP_WINDOW = 1e3;
|
|
5
|
+
var GanttHistory = class {
|
|
6
|
+
depth;
|
|
7
|
+
done = [];
|
|
8
|
+
undone = [];
|
|
9
|
+
constructor(depth = 100) {
|
|
10
|
+
this.depth = depth;
|
|
11
|
+
}
|
|
12
|
+
get canUndo() {
|
|
13
|
+
return this.done.length > 0;
|
|
14
|
+
}
|
|
15
|
+
get canRedo() {
|
|
16
|
+
return this.undone.length > 0;
|
|
17
|
+
}
|
|
18
|
+
record(tr, selectionBefore, selectionAfter, now = Date.now()) {
|
|
19
|
+
if (!tr.changed || tr.meta.history === false) return;
|
|
20
|
+
this.undone = [];
|
|
21
|
+
const last = this.done[this.done.length - 1];
|
|
22
|
+
const group = tr.meta.group;
|
|
23
|
+
if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {
|
|
24
|
+
last.steps = compactSteps([...last.steps, ...tr.steps]);
|
|
25
|
+
last.inverse = compactSteps([...tr.inverse, ...last.inverse]);
|
|
26
|
+
last.time = now;
|
|
27
|
+
last.selectionAfter = selectionAfter;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.done.push({
|
|
31
|
+
steps: tr.steps.slice(),
|
|
32
|
+
inverse: tr.inverse.slice(),
|
|
33
|
+
group,
|
|
34
|
+
time: now,
|
|
35
|
+
selectionBefore,
|
|
36
|
+
selectionAfter
|
|
37
|
+
});
|
|
38
|
+
if (this.done.length > this.depth) this.done.shift();
|
|
39
|
+
}
|
|
40
|
+
undo(doc) {
|
|
41
|
+
const entry = this.done.pop();
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
const tr = applyGanttSteps(doc, entry.inverse);
|
|
44
|
+
this.undone.push(entry);
|
|
45
|
+
return {
|
|
46
|
+
tr,
|
|
47
|
+
selection: entry.selectionBefore
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
redo(doc) {
|
|
51
|
+
const entry = this.undone.pop();
|
|
52
|
+
if (!entry) return null;
|
|
53
|
+
const tr = applyGanttSteps(doc, entry.steps);
|
|
54
|
+
this.done.push({
|
|
55
|
+
...entry,
|
|
56
|
+
group: void 0
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
tr,
|
|
60
|
+
selection: entry.selectionAfter
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
clear() {
|
|
64
|
+
this.done = [];
|
|
65
|
+
this.undone = [];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
export { GanttHistory };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","names":[],"sources":["../../src/core/history.ts"],"sourcesContent":["import {\n applyGanttSteps,\n compactSteps,\n type GanttDocument,\n type GanttStep,\n type GanttTransaction,\n} from './transaction'\n\ninterface Entry {\n steps: GanttStep[]\n inverse: GanttStep[]\n group?: string\n time: number\n selectionBefore: string[]\n selectionAfter: string[]\n}\n\n/** Transactions in one group merge when they come within this many ms. */\nconst GROUP_WINDOW = 1000\n\nexport class GanttHistory {\n private done: Entry[] = []\n private undone: Entry[] = []\n\n constructor(private depth = 100) {}\n\n get canUndo() {\n return this.done.length > 0\n }\n\n get canRedo() {\n return this.undone.length > 0\n }\n\n record(\n tr: GanttTransaction,\n selectionBefore: string[],\n selectionAfter: string[],\n now = Date.now(),\n ) {\n if (!tr.changed || tr.meta.history === false) return\n this.undone = []\n const last = this.done[this.done.length - 1]\n const group = tr.meta.group\n if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {\n last.steps = compactSteps([...last.steps, ...tr.steps])\n last.inverse = compactSteps([...tr.inverse, ...last.inverse])\n last.time = now\n last.selectionAfter = selectionAfter\n return\n }\n this.done.push({\n steps: tr.steps.slice(),\n inverse: tr.inverse.slice(),\n group,\n time: now,\n selectionBefore,\n selectionAfter,\n })\n if (this.done.length > this.depth) this.done.shift()\n }\n\n undo(doc: GanttDocument) {\n const entry = this.done.pop()\n if (!entry) return null\n const tr = applyGanttSteps(doc, entry.inverse)\n this.undone.push(entry)\n return { tr, selection: entry.selectionBefore }\n }\n\n redo(doc: GanttDocument) {\n const entry = this.undone.pop()\n if (!entry) return null\n const tr = applyGanttSteps(doc, entry.steps)\n this.done.push({ ...entry, group: undefined })\n return { tr, selection: entry.selectionAfter }\n }\n\n clear() {\n this.done = []\n this.undone = []\n }\n}\n"],"mappings":";;;AAkBA,IAAM,eAAe;AAErB,IAAa,eAAb,MAA0B;CAIJ;CAHpB,OAAwB,CAAC;CACzB,SAA0B,CAAC;CAE3B,YAAY,QAAgB,KAAK;EAAb,KAAA,QAAA;CAAc;CAElC,IAAI,UAAU;EACZ,OAAO,KAAK,KAAK,SAAS;CAC5B;CAEA,IAAI,UAAU;EACZ,OAAO,KAAK,OAAO,SAAS;CAC9B;CAEA,OACE,IACA,iBACA,gBACA,MAAM,KAAK,IAAI,GACf;EACA,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,YAAY,OAAO;EAC9C,KAAK,SAAS,CAAC;EACf,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS;EAC1C,MAAM,QAAQ,GAAG,KAAK;EACtB,IAAI,QAAQ,SAAS,KAAK,UAAU,SAAS,MAAM,KAAK,OAAO,cAAc;GAC3E,KAAK,QAAQ,aAAa,CAAC,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,CAAC;GACtD,KAAK,UAAU,aAAa,CAAC,GAAG,GAAG,SAAS,GAAG,KAAK,OAAO,CAAC;GAC5D,KAAK,OAAO;GACZ,KAAK,iBAAiB;GACtB;EACF;EACA,KAAK,KAAK,KAAK;GACb,OAAO,GAAG,MAAM,MAAM;GACtB,SAAS,GAAG,QAAQ,MAAM;GAC1B;GACA,MAAM;GACN;GACA;EACF,CAAC;EACD,IAAI,KAAK,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,MAAM;CACrD;CAEA,KAAK,KAAoB;EACvB,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,KAAK,gBAAgB,KAAK,MAAM,OAAO;EAC7C,KAAK,OAAO,KAAK,KAAK;EACtB,OAAO;GAAE;GAAI,WAAW,MAAM;EAAgB;CAChD;CAEA,KAAK,KAAoB;EACvB,MAAM,QAAQ,KAAK,OAAO,IAAI;EAC9B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,KAAK,gBAAgB,KAAK,MAAM,KAAK;EAC3C,KAAK,KAAK,KAAK;GAAE,GAAG;GAAO,OAAO,KAAA;EAAU,CAAC;EAC7C,OAAO;GAAE;GAAI,WAAW,MAAM;EAAe;CAC/C;CAEA,QAAQ;EACN,KAAK,OAAO,CAAC;EACb,KAAK,SAAS,CAAC;CACjB;AACF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/core/links.ts
|
|
2
|
+
/** Whether `to` can be reached from `from` along the links. */
|
|
3
|
+
function reaches(links, from, to) {
|
|
4
|
+
const next = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const link of links) {
|
|
6
|
+
const list = next.get(link.from);
|
|
7
|
+
if (list) list.push(link.to);
|
|
8
|
+
else next.set(link.from, [link.to]);
|
|
9
|
+
}
|
|
10
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11
|
+
const stack = [from];
|
|
12
|
+
while (stack.length) {
|
|
13
|
+
const id = stack.pop();
|
|
14
|
+
if (id === to) return true;
|
|
15
|
+
if (seen.has(id)) continue;
|
|
16
|
+
seen.add(id);
|
|
17
|
+
stack.push(...next.get(id) ?? []);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/** Whether `ancestor` is a parent, grandparent… of `id`. */
|
|
22
|
+
function isAncestor(tasks, ancestor, id) {
|
|
23
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24
|
+
for (let up = tasks.get(id)?.parent; up != null && !seen.has(up); up = tasks.get(up)?.parent) {
|
|
25
|
+
if (up === ancestor) return true;
|
|
26
|
+
seen.add(up);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Why a link from `from` to `to` can't be made, or `null` when it can: a
|
|
32
|
+
* task can't depend on itself, on its own parent or child, on a task it
|
|
33
|
+
* already depends on, or on a task that waits for it.
|
|
34
|
+
*/
|
|
35
|
+
function linkRefusal(tasks, links, from, to) {
|
|
36
|
+
if (from === to) return "self";
|
|
37
|
+
if (isAncestor(tasks, from, to) || isAncestor(tasks, to, from)) return "nested";
|
|
38
|
+
if (links.some((link) => link.from === from && link.to === to || link.from === to && link.to === from)) return "duplicate";
|
|
39
|
+
if (reaches(links, to, from)) return "cycle";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** The link type for a line drawn from one end of a task to one end of another. */
|
|
43
|
+
function linkTypeOf(fromEnd, toEnd) {
|
|
44
|
+
if (fromEnd === "end") return toEnd === "start" ? "fs" : "ff";
|
|
45
|
+
return toEnd === "start" ? "ss" : "sf";
|
|
46
|
+
}
|
|
47
|
+
/** Reads a `lag`: a number of working days, or `'2d'`, `'4h'`, `'30m'`; `null` when unreadable. */
|
|
48
|
+
function parseLag(lag) {
|
|
49
|
+
if (lag == null || lag === "") return {
|
|
50
|
+
amount: 0,
|
|
51
|
+
unit: "day"
|
|
52
|
+
};
|
|
53
|
+
if (typeof lag === "number") return Number.isFinite(lag) ? {
|
|
54
|
+
amount: lag,
|
|
55
|
+
unit: "day"
|
|
56
|
+
} : null;
|
|
57
|
+
const match = /^\s*(-?\d+(?:\.\d+)?)\s*(d|h|m)\s*$/i.exec(lag);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const value = Number(match[1]);
|
|
60
|
+
const unit = match[2].toLowerCase();
|
|
61
|
+
if (unit === "d") return {
|
|
62
|
+
amount: value,
|
|
63
|
+
unit: "day"
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
amount: Math.round(unit === "h" ? value * 60 : value),
|
|
67
|
+
unit: "minute"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export { linkRefusal, linkTypeOf, parseLag, reaches };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","names":[],"sources":["../../src/core/links.ts"],"sourcesContent":["import type { GanttLink, GanttLinkType, GanttTask } from './model'\n\n/** Why a new link was refused. */\nexport type GanttLinkRefusal = 'self' | 'nested' | 'duplicate' | 'cycle' | 'custom'\n\n/** Whether `to` can be reached from `from` along the links. */\nexport function reaches(links: readonly GanttLink[], from: string, to: string) {\n const next = new Map<string, string[]>()\n for (const link of links) {\n const list = next.get(link.from)\n if (list) list.push(link.to)\n else next.set(link.from, [link.to])\n }\n const seen = new Set<string>()\n const stack = [from]\n while (stack.length) {\n const id = stack.pop()!\n if (id === to) return true\n if (seen.has(id)) continue\n seen.add(id)\n stack.push(...(next.get(id) ?? []))\n }\n return false\n}\n\n/** Whether `ancestor` is a parent, grandparent… of `id`. */\nfunction isAncestor(tasks: ReadonlyMap<string, GanttTask>, ancestor: string, id: string) {\n const seen = new Set<string>()\n for (let up = tasks.get(id)?.parent; up != null && !seen.has(up); up = tasks.get(up)?.parent) {\n if (up === ancestor) return true\n seen.add(up)\n }\n return false\n}\n\n/**\n * Why a link from `from` to `to` can't be made, or `null` when it can: a\n * task can't depend on itself, on its own parent or child, on a task it\n * already depends on, or on a task that waits for it.\n */\nexport function linkRefusal(\n tasks: ReadonlyMap<string, GanttTask>,\n links: readonly GanttLink[],\n from: string,\n to: string,\n): GanttLinkRefusal | null {\n if (from === to) return 'self'\n if (isAncestor(tasks, from, to) || isAncestor(tasks, to, from)) return 'nested'\n if (\n links.some(\n (link) => (link.from === from && link.to === to) || (link.from === to && link.to === from),\n )\n )\n return 'duplicate'\n if (reaches(links, to, from)) return 'cycle'\n return null\n}\n\n/** The link type for a line drawn from one end of a task to one end of another. */\nexport function linkTypeOf(fromEnd: 'start' | 'end', toEnd: 'start' | 'end'): GanttLinkType {\n if (fromEnd === 'end') return toEnd === 'start' ? 'fs' : 'ff'\n return toEnd === 'start' ? 'ss' : 'sf'\n}\n\n/** A link's lag, read: an amount of working days, or of working minutes. */\nexport interface GanttLag {\n amount: number\n unit: 'day' | 'minute'\n}\n\n/** Reads a `lag`: a number of working days, or `'2d'`, `'4h'`, `'30m'`; `null` when unreadable. */\nexport function parseLag(lag: number | string | undefined): GanttLag | null {\n if (lag == null || lag === '') return { amount: 0, unit: 'day' }\n if (typeof lag === 'number') return Number.isFinite(lag) ? { amount: lag, unit: 'day' } : null\n const match = /^\\s*(-?\\d+(?:\\.\\d+)?)\\s*(d|h|m)\\s*$/i.exec(lag)\n if (!match) return null\n const value = Number(match[1])\n const unit = match[2]!.toLowerCase()\n if (unit === 'd') return { amount: value, unit: 'day' }\n return { amount: Math.round(unit === 'h' ? value * 60 : value), unit: 'minute' }\n}\n"],"mappings":";;AAMA,SAAgB,QAAQ,OAA6B,MAAc,IAAY;CAC7E,MAAM,uBAAO,IAAI,IAAsB;CACvC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,IAAI,KAAK,IAAI;EAC/B,IAAI,MAAM,KAAK,KAAK,KAAK,EAAE;OACtB,KAAK,IAAI,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;CACpC;CACA,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAAQ,CAAC,IAAI;CACnB,OAAO,MAAM,QAAQ;EACnB,MAAM,KAAK,MAAM,IAAI;EACrB,IAAI,OAAO,IAAI,OAAO;EACtB,IAAI,KAAK,IAAI,EAAE,GAAG;EAClB,KAAK,IAAI,EAAE;EACX,MAAM,KAAK,GAAI,KAAK,IAAI,EAAE,KAAK,CAAC,CAAE;CACpC;CACA,OAAO;AACT;;AAGA,SAAS,WAAW,OAAuC,UAAkB,IAAY;CACvF,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,MAAM,QAAQ,CAAC,KAAK,IAAI,EAAE,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ;EAC5F,IAAI,OAAO,UAAU,OAAO;EAC5B,KAAK,IAAI,EAAE;CACb;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,YACd,OACA,OACA,MACA,IACyB;CACzB,IAAI,SAAS,IAAI,OAAO;CACxB,IAAI,WAAW,OAAO,MAAM,EAAE,KAAK,WAAW,OAAO,IAAI,IAAI,GAAG,OAAO;CACvE,IACE,MAAM,MACH,SAAU,KAAK,SAAS,QAAQ,KAAK,OAAO,MAAQ,KAAK,SAAS,MAAM,KAAK,OAAO,IACvF,GAEA,OAAO;CACT,IAAI,QAAQ,OAAO,IAAI,IAAI,GAAG,OAAO;CACrC,OAAO;AACT;;AAGA,SAAgB,WAAW,SAA0B,OAAuC;CAC1F,IAAI,YAAY,OAAO,OAAO,UAAU,UAAU,OAAO;CACzD,OAAO,UAAU,UAAU,OAAO;AACpC;;AASA,SAAgB,SAAS,KAAmD;CAC1E,IAAI,OAAO,QAAQ,QAAQ,IAAI,OAAO;EAAE,QAAQ;EAAG,MAAM;CAAM;CAC/D,IAAI,OAAO,QAAQ,UAAU,OAAO,OAAO,SAAS,GAAG,IAAI;EAAE,QAAQ;EAAK,MAAM;CAAM,IAAI;CAC1F,MAAM,QAAQ,uCAAuC,KAAK,GAAG;CAC7D,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,QAAQ,OAAO,MAAM,EAAE;CAC7B,MAAM,OAAO,MAAM,EAAE,CAAE,YAAY;CACnC,IAAI,SAAS,KAAK,OAAO;EAAE,QAAQ;EAAO,MAAM;CAAM;CACtD,OAAO;EAAE,QAAQ,KAAK,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK;EAAG,MAAM;CAAS;AACjF"}
|