@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 daidr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dou-ui/gantt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[DouUI](https://github.com/daidr/dou-ui) 的甘特图与资源排期:任务排期、依赖、工作日历与自动排期,支持协同编辑。
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun add @dou-ui/gantt dou-ui
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
用法见 [文档](https://ui.daidr.me/gantt/)。
|
|
10
|
+
|
|
11
|
+
许可证:MIT
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { defineGanttExtension } from "../core/extension.js";
|
|
2
|
+
import { buildTree } from "../core/tree.js";
|
|
3
|
+
import { isOrderKey, orderKeys } from "../core/order.js";
|
|
4
|
+
import { createPeers } from "./peers.js";
|
|
5
|
+
import { collaborationColors } from "dou-ui";
|
|
6
|
+
import { toRaw, watch } from "vue";
|
|
7
|
+
import * as Y from "yjs";
|
|
8
|
+
// src/collaboration/index.ts
|
|
9
|
+
/** Folding is each person's own view: it stays out of the shared document. */
|
|
10
|
+
var LOCAL_FIELDS = /* @__PURE__ */ new Set(["id", "collapsed"]);
|
|
11
|
+
var same = (a, b) => a === b || typeof a === "object" && JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
+
/** A task or link as the map holds it, or `null` when it holds none. */
|
|
13
|
+
function read(map, id) {
|
|
14
|
+
const fields = map.get(id);
|
|
15
|
+
if (!(fields instanceof Y.Map)) return null;
|
|
16
|
+
return {
|
|
17
|
+
...fields.toJSON(),
|
|
18
|
+
id
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Where in `value` there is something JSON can't hold (a function, a component, a `Date`…), or `null`. */
|
|
22
|
+
function unsharable(value, path, seen = /* @__PURE__ */ new Set()) {
|
|
23
|
+
if (value === null || [
|
|
24
|
+
"string",
|
|
25
|
+
"number",
|
|
26
|
+
"boolean",
|
|
27
|
+
"undefined"
|
|
28
|
+
].includes(typeof value)) return null;
|
|
29
|
+
if (typeof value !== "object") return path;
|
|
30
|
+
const proto = Object.getPrototypeOf(value);
|
|
31
|
+
if (seen.has(value)) return path;
|
|
32
|
+
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return path;
|
|
33
|
+
seen.add(value);
|
|
34
|
+
for (const [key, item] of Object.entries(value)) {
|
|
35
|
+
const found = unsharable(item, Array.isArray(value) ? `${path}[${key}]` : `${path}.${key}`, seen);
|
|
36
|
+
if (found) return found;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
var warned = /* @__PURE__ */ new Set();
|
|
41
|
+
function checkSharable(id, key, value) {
|
|
42
|
+
if (process.env.NODE_ENV === "production" || warned.has(`${id}:${key}`)) return;
|
|
43
|
+
const path = unsharable(toRaw(value), key);
|
|
44
|
+
if (!path) return;
|
|
45
|
+
warned.add(`${id}:${key}`);
|
|
46
|
+
console.warn(`[@dou-ui/gantt] "${id}" has a value at ${path} that can't be shared: only JSON values reach the others. Keep components and functions out of the shared fields, for example by looking them up by name.`);
|
|
47
|
+
}
|
|
48
|
+
/** Writes a task's or link's fields, only those that changed. */
|
|
49
|
+
function write(map, value) {
|
|
50
|
+
const item = value;
|
|
51
|
+
const existing = map.get(value.id);
|
|
52
|
+
let fields;
|
|
53
|
+
if (existing instanceof Y.Map) fields = existing;
|
|
54
|
+
else map.set(value.id, fields = new Y.Map());
|
|
55
|
+
for (const [key, field] of Object.entries(item)) {
|
|
56
|
+
if (LOCAL_FIELDS.has(key) || field === void 0) continue;
|
|
57
|
+
checkSharable(value.id, key, field);
|
|
58
|
+
if (!same(fields.get(key), field)) fields.set(key, field);
|
|
59
|
+
}
|
|
60
|
+
for (const key of Array.from(fields.keys())) if (!LOCAL_FIELDS.has(key) && item[key] === void 0) fields.delete(key);
|
|
61
|
+
}
|
|
62
|
+
/** The change that makes `local` match `shared`, keeping the local fields. */
|
|
63
|
+
function patchFor(local, shared) {
|
|
64
|
+
const a = local;
|
|
65
|
+
const b = shared;
|
|
66
|
+
const patch = {};
|
|
67
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
68
|
+
if (LOCAL_FIELDS.has(key)) continue;
|
|
69
|
+
if (!same(a[key], b[key])) patch[key] = b[key];
|
|
70
|
+
}
|
|
71
|
+
return Object.keys(patch).length ? patch : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Gives every task an `order` key, so that siblings sort alike for
|
|
75
|
+
* everyone. Siblings whose keys are already in order keep them.
|
|
76
|
+
*/
|
|
77
|
+
function withOrder(tasks) {
|
|
78
|
+
const tree = buildTree(tasks);
|
|
79
|
+
const keys = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const siblings of tree.childrenOf.values()) {
|
|
81
|
+
if (siblings.every((task, i) => task.order != null && isOrderKey(task.order) && (i === 0 || siblings[i - 1].order < task.order))) continue;
|
|
82
|
+
const fresh = orderKeys(siblings.length);
|
|
83
|
+
siblings.forEach((task, i) => keys.set(task.id, fresh[i]));
|
|
84
|
+
}
|
|
85
|
+
return keys.size ? tasks.map((task) => keys.has(task.id) ? {
|
|
86
|
+
...task,
|
|
87
|
+
order: keys.get(task.id)
|
|
88
|
+
} : task) : tasks;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Writes tasks and links into empty maps, e.g. when a plan that so far was
|
|
92
|
+
* saved as JSON starts being edited together. Every task gets an `order`
|
|
93
|
+
* key. Do it once, where the shared document is created.
|
|
94
|
+
*/
|
|
95
|
+
function fillMaps(maps, doc) {
|
|
96
|
+
const fill = () => {
|
|
97
|
+
for (const task of withOrder(doc.tasks)) write(maps.tasks, task);
|
|
98
|
+
for (const link of doc.links) write(maps.links, link);
|
|
99
|
+
};
|
|
100
|
+
const ydoc = maps.tasks.doc;
|
|
101
|
+
if (ydoc) ydoc.transact(fill);
|
|
102
|
+
else fill();
|
|
103
|
+
}
|
|
104
|
+
/** The tasks and links a transaction added, removed or changed. */
|
|
105
|
+
function touched(steps) {
|
|
106
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
107
|
+
const links = /* @__PURE__ */ new Set();
|
|
108
|
+
for (const step of steps) if (step.type === "addTask") tasks.add(step.task.id);
|
|
109
|
+
else if (step.type === "addLink") links.add(step.link.id);
|
|
110
|
+
else if (step.type === "removeTask" || step.type === "updateTask") tasks.add(step.id);
|
|
111
|
+
else links.add(step.id);
|
|
112
|
+
return {
|
|
113
|
+
tasks,
|
|
114
|
+
links
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Gives the tasks a transaction added without an `order` key one, where they sort now. */
|
|
118
|
+
function orderAdded(tr) {
|
|
119
|
+
for (const step of tr.steps.slice()) {
|
|
120
|
+
if (step.type !== "addTask") continue;
|
|
121
|
+
const task = tr.getTask(step.task.id);
|
|
122
|
+
if (!task || task.order != null) continue;
|
|
123
|
+
const parent = task.parent != null && tr.getTask(task.parent) ? task.parent : void 0;
|
|
124
|
+
const siblings = buildTree(tr.doc.tasks).childrenOf.get(parent) ?? [];
|
|
125
|
+
tr.moveTask(task.id, parent, siblings.findIndex((item) => item.id === task.id));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Edit a Gantt chart together through a Yjs document. The tasks and links
|
|
130
|
+
* live in two `Y.Map`s: the chart's `v-model` only mirrors them, and
|
|
131
|
+
* changes made to `v-model` from outside are not written back. Undo only
|
|
132
|
+
* takes back this user's own changes. Folding a summary stays local.
|
|
133
|
+
*/
|
|
134
|
+
function collaboration(options) {
|
|
135
|
+
const { tasks, links } = options;
|
|
136
|
+
const ydoc = tasks.doc;
|
|
137
|
+
if (!ydoc || links.doc !== ydoc) throw new Error("[@dou-ui/gantt] tasks and links must be maps of the same Y.Doc");
|
|
138
|
+
const { awareness, user } = options;
|
|
139
|
+
if (awareness && user) {
|
|
140
|
+
const color = user.color ?? collaborationColors[ydoc.clientID % collaborationColors.length];
|
|
141
|
+
awareness.setLocalStateField("user", {
|
|
142
|
+
name: user.name,
|
|
143
|
+
color
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** This chart's own changes, which undo can take back. */
|
|
147
|
+
const tracked = { gantt: "tracked" };
|
|
148
|
+
/** Changes that undo leaves alone, e.g. filling the maps. */
|
|
149
|
+
const untracked = { gantt: "untracked" };
|
|
150
|
+
const undoManager = new Y.UndoManager([tasks, links], {
|
|
151
|
+
trackedOrigins: /* @__PURE__ */ new Set([tracked]),
|
|
152
|
+
captureTimeout: 500
|
|
153
|
+
});
|
|
154
|
+
/** Set while shared changes are applied to the chart, so they aren't written back. */
|
|
155
|
+
let applying = false;
|
|
156
|
+
/** What undo or redo is running, to name the change. */
|
|
157
|
+
let travelling = null;
|
|
158
|
+
let lastGroup;
|
|
159
|
+
function writeChanges(tr) {
|
|
160
|
+
const { tasks: taskIds, links: linkIds } = touched(tr.steps);
|
|
161
|
+
const taskById = new Map(tr.doc.tasks.map((task) => [task.id, task]));
|
|
162
|
+
const linkById = new Map(tr.doc.links.map((link) => [link.id, link]));
|
|
163
|
+
const tracking = tr.meta.history !== false;
|
|
164
|
+
if (tracking && (!tr.meta.group || tr.meta.group !== lastGroup)) undoManager.stopCapturing();
|
|
165
|
+
if (tracking) lastGroup = tr.meta.group;
|
|
166
|
+
ydoc.transact(() => {
|
|
167
|
+
for (const id of taskIds) {
|
|
168
|
+
const task = taskById.get(id);
|
|
169
|
+
if (task) write(tasks, task);
|
|
170
|
+
else tasks.delete(id);
|
|
171
|
+
}
|
|
172
|
+
for (const id of linkIds) {
|
|
173
|
+
const link = linkById.get(id);
|
|
174
|
+
if (link) write(links, link);
|
|
175
|
+
else links.delete(id);
|
|
176
|
+
}
|
|
177
|
+
}, tracking ? tracked : untracked);
|
|
178
|
+
}
|
|
179
|
+
/** Adds steps to `tr` that bring the given tasks and links in line with the maps. */
|
|
180
|
+
function pull(tr, taskIds, linkIds) {
|
|
181
|
+
for (const id of taskIds) {
|
|
182
|
+
const shared = read(tasks, id);
|
|
183
|
+
const local = tr.getTask(id);
|
|
184
|
+
if (!shared) {
|
|
185
|
+
if (local) tr.step({
|
|
186
|
+
type: "removeTask",
|
|
187
|
+
id
|
|
188
|
+
});
|
|
189
|
+
} else if (!local) tr.step({
|
|
190
|
+
type: "addTask",
|
|
191
|
+
task: shared
|
|
192
|
+
});
|
|
193
|
+
else {
|
|
194
|
+
const patch = patchFor(local, shared);
|
|
195
|
+
if (patch) tr.step({
|
|
196
|
+
type: "updateTask",
|
|
197
|
+
id,
|
|
198
|
+
patch
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const id of linkIds) {
|
|
203
|
+
const shared = read(links, id);
|
|
204
|
+
const local = tr.getLink(id);
|
|
205
|
+
if (!shared) {
|
|
206
|
+
if (local) tr.step({
|
|
207
|
+
type: "removeLink",
|
|
208
|
+
id
|
|
209
|
+
});
|
|
210
|
+
} else if (!local) tr.step({
|
|
211
|
+
type: "addLink",
|
|
212
|
+
link: shared
|
|
213
|
+
});
|
|
214
|
+
else {
|
|
215
|
+
const patch = patchFor(local, shared);
|
|
216
|
+
if (patch) tr.step({
|
|
217
|
+
type: "updateLink",
|
|
218
|
+
id,
|
|
219
|
+
patch
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return defineGanttExtension({
|
|
225
|
+
name: "collaboration",
|
|
226
|
+
priority: 0,
|
|
227
|
+
transaction(tr) {
|
|
228
|
+
if (applying) return;
|
|
229
|
+
orderAdded(tr);
|
|
230
|
+
writeChanges(tr);
|
|
231
|
+
},
|
|
232
|
+
history: () => ({
|
|
233
|
+
undo: () => run("undo"),
|
|
234
|
+
redo: () => run("redo"),
|
|
235
|
+
canUndo: () => undoManager.undoStack.length > 0,
|
|
236
|
+
canRedo: () => undoManager.redoStack.length > 0
|
|
237
|
+
}),
|
|
238
|
+
layers: awareness ? { above: [createPeers(awareness)] } : void 0,
|
|
239
|
+
setup(context) {
|
|
240
|
+
const apply = (fn, origin) => {
|
|
241
|
+
applying = true;
|
|
242
|
+
try {
|
|
243
|
+
context.transact(fn, {
|
|
244
|
+
history: false,
|
|
245
|
+
schedule: false,
|
|
246
|
+
origin
|
|
247
|
+
});
|
|
248
|
+
} finally {
|
|
249
|
+
applying = false;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const current = () => ({
|
|
253
|
+
version: 1,
|
|
254
|
+
tasks: toRaw(context.tasks.value),
|
|
255
|
+
links: toRaw(context.links.value)
|
|
256
|
+
});
|
|
257
|
+
if (!tasks.size && !links.size) ydoc.transact(() => fillMaps({
|
|
258
|
+
tasks,
|
|
259
|
+
links
|
|
260
|
+
}, current()), untracked);
|
|
261
|
+
const doc = current();
|
|
262
|
+
apply((tr) => pull(tr, /* @__PURE__ */ new Set([...doc.tasks.map((task) => task.id), ...tasks.keys()]), /* @__PURE__ */ new Set([...doc.links.map((link) => link.id), ...links.keys()])), "remote");
|
|
263
|
+
const pending = {
|
|
264
|
+
tasks: /* @__PURE__ */ new Set(),
|
|
265
|
+
links: /* @__PURE__ */ new Set()
|
|
266
|
+
};
|
|
267
|
+
const gather = (map, into) => (events, transaction) => {
|
|
268
|
+
if (transaction.origin === tracked || transaction.origin === untracked) return;
|
|
269
|
+
for (const event of events) if (event.target === map) for (const key of event.changes.keys.keys()) into.add(key);
|
|
270
|
+
else if (event.path.length) into.add(String(event.path[0]));
|
|
271
|
+
};
|
|
272
|
+
const onTasks = gather(tasks, pending.tasks);
|
|
273
|
+
const onLinks = gather(links, pending.links);
|
|
274
|
+
const flush = () => {
|
|
275
|
+
if (!pending.tasks.size && !pending.links.size) return;
|
|
276
|
+
const taskIds = [...pending.tasks];
|
|
277
|
+
const linkIds = [...pending.links];
|
|
278
|
+
pending.tasks.clear();
|
|
279
|
+
pending.links.clear();
|
|
280
|
+
apply((tr) => pull(tr, taskIds, linkIds), travelling ?? "remote");
|
|
281
|
+
};
|
|
282
|
+
tasks.observeDeep(onTasks);
|
|
283
|
+
links.observeDeep(onLinks);
|
|
284
|
+
ydoc.on("afterTransaction", flush);
|
|
285
|
+
let stopPresence;
|
|
286
|
+
if (awareness) stopPresence = watch([context.preview, context.selection], ([preview, selection]) => {
|
|
287
|
+
const moving = [];
|
|
288
|
+
for (const [id, patch] of preview ?? []) {
|
|
289
|
+
if (moving.length >= 100) break;
|
|
290
|
+
moving.push({
|
|
291
|
+
id,
|
|
292
|
+
start: patch.start,
|
|
293
|
+
end: patch.end
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
awareness.setLocalStateField("gantt", {
|
|
297
|
+
moving,
|
|
298
|
+
selection: selection.slice(0, 100)
|
|
299
|
+
});
|
|
300
|
+
}, { immediate: true });
|
|
301
|
+
return () => {
|
|
302
|
+
tasks.unobserveDeep(onTasks);
|
|
303
|
+
links.unobserveDeep(onLinks);
|
|
304
|
+
ydoc.off("afterTransaction", flush);
|
|
305
|
+
stopPresence?.();
|
|
306
|
+
awareness?.setLocalStateField("gantt", null);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
function run(action) {
|
|
311
|
+
if (!(action === "undo" ? undoManager.undoStack : undoManager.redoStack).length) return false;
|
|
312
|
+
travelling = action;
|
|
313
|
+
try {
|
|
314
|
+
undoManager[action]();
|
|
315
|
+
} finally {
|
|
316
|
+
travelling = null;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export { collaboration, collaborationColors, fillMaps };
|
|
322
|
+
|
|
323
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/collaboration/index.ts"],"sourcesContent":["import { collaborationColors } from 'dou-ui'\nimport { toRaw, watch } from 'vue'\nimport * as Y from 'yjs'\nimport { defineGanttExtension, type GanttExtension } from '../core/extension'\nimport type { GanttLink, GanttTask } from '../core/model'\nimport { isOrderKey, orderKeys } from '../core/order'\nimport type { GanttDocument, GanttStep, GanttTransaction } from '../core/transaction'\nimport { buildTree } from '../core/tree'\nimport { createPeers, type AwarenessLike, type GanttPresence } from './peers'\n\n/** How the people editing together see each other. */\nexport interface CollaborationUser {\n name: string\n /** The color of this user's outlines, any CSS color. Picked from the client id when left out. */\n color?: string\n}\n\nexport interface CollaborationOptions {\n /** Where the tasks live, e.g. `ydoc.getMap('tasks')`. */\n tasks: Y.Map<unknown>\n /** Where the links live, e.g. `ydoc.getMap('links')`; in the same `Y.Doc` as `tasks`. */\n links: Y.Map<unknown>\n /** Shares what each person selects and drags (`y-protocols/awareness`). */\n awareness?: AwarenessLike\n /** This user, shown to the others. Needs `awareness`. */\n user?: CollaborationUser\n}\n\nexport { collaborationColors }\n\ntype Shared = Y.Map<unknown>\ntype Item = Record<string, unknown>\n\n/** Folding is each person's own view: it stays out of the shared document. */\nconst LOCAL_FIELDS = new Set(['id', 'collapsed'])\n\nconst same = (a: unknown, b: unknown) =>\n a === b || (typeof a === 'object' && JSON.stringify(a) === JSON.stringify(b))\n\n/** A task or link as the map holds it, or `null` when it holds none. */\nfunction read<T>(map: Shared, id: string): T | null {\n const fields = map.get(id)\n if (!(fields instanceof Y.Map)) return null\n return { ...(fields.toJSON() as Item), id } as T\n}\n\n/** Where in `value` there is something JSON can't hold (a function, a component, a `Date`…), or `null`. */\nfunction unsharable(value: unknown, path: string, seen = new Set<object>()): string | null {\n if (value === null || ['string', 'number', 'boolean', 'undefined'].includes(typeof value))\n return null\n if (typeof value !== 'object') return path\n const proto = Object.getPrototypeOf(value)\n if (seen.has(value)) return path\n if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return path\n seen.add(value)\n for (const [key, item] of Object.entries(value)) {\n const found = unsharable(\n item,\n Array.isArray(value) ? `${path}[${key}]` : `${path}.${key}`,\n seen,\n )\n if (found) return found\n }\n return null\n}\n\n/*\n * Such a value reads back fine on this side, but the others get it without\n * the parts JSON can't hold: say so once per field while developing.\n */\nconst warned = new Set<string>()\nfunction checkSharable(id: string, key: string, value: unknown) {\n // the app's bundler replaces this, as for Vue's own warnings\n if (process.env.NODE_ENV === 'production' || warned.has(`${id}:${key}`)) return\n const path = unsharable(toRaw(value), key)\n if (!path) return\n warned.add(`${id}:${key}`)\n console.warn(\n `[@dou-ui/gantt] \"${id}\" has a value at ${path} that can't be shared: only JSON values reach the others. Keep components and functions out of the shared fields, for example by looking them up by name.`,\n )\n}\n\n/** Writes a task's or link's fields, only those that changed. */\nfunction write(map: Shared, value: GanttTask | GanttLink) {\n const item = value as unknown as Item\n const existing = map.get(value.id)\n let fields: Shared\n if (existing instanceof Y.Map) fields = existing\n // a new map joins the document first: it can't be read before\n else map.set(value.id, (fields = new Y.Map<unknown>()))\n for (const [key, field] of Object.entries(item)) {\n if (LOCAL_FIELDS.has(key) || field === undefined) continue\n checkSharable(value.id, key, field)\n if (!same(fields.get(key), field)) fields.set(key, field)\n }\n for (const key of Array.from(fields.keys()))\n if (!LOCAL_FIELDS.has(key) && item[key] === undefined) fields.delete(key)\n}\n\n/** The change that makes `local` match `shared`, keeping the local fields. */\nfunction patchFor<T extends object>(local: T, shared: T) {\n const a = local as Item\n const b = shared as Item\n const patch: Item = {}\n for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {\n if (LOCAL_FIELDS.has(key)) continue\n if (!same(a[key], b[key])) patch[key] = b[key]\n }\n return Object.keys(patch).length ? (patch as Partial<T>) : null\n}\n\n/**\n * Gives every task an `order` key, so that siblings sort alike for\n * everyone. Siblings whose keys are already in order keep them.\n */\nfunction withOrder(tasks: GanttTask[]): GanttTask[] {\n const tree = buildTree(tasks)\n const keys = new Map<string, string>()\n for (const siblings of tree.childrenOf.values()) {\n const ordered = siblings.every(\n (task, i) =>\n task.order != null &&\n isOrderKey(task.order) &&\n (i === 0 || siblings[i - 1]!.order! < task.order),\n )\n if (ordered) continue\n const fresh = orderKeys(siblings.length)\n siblings.forEach((task, i) => keys.set(task.id, fresh[i]!))\n }\n return keys.size\n ? tasks.map((task) => (keys.has(task.id) ? { ...task, order: keys.get(task.id) } : task))\n : tasks\n}\n\n/**\n * Writes tasks and links into empty maps, e.g. when a plan that so far was\n * saved as JSON starts being edited together. Every task gets an `order`\n * key. Do it once, where the shared document is created.\n */\nexport function fillMaps(\n maps: { tasks: Y.Map<unknown>; links: Y.Map<unknown> },\n doc: GanttDocument,\n) {\n const fill = () => {\n for (const task of withOrder(doc.tasks)) write(maps.tasks, task)\n for (const link of doc.links) write(maps.links, link)\n }\n const ydoc = maps.tasks.doc\n if (ydoc) ydoc.transact(fill)\n else fill()\n}\n\n/** The tasks and links a transaction added, removed or changed. */\nfunction touched(steps: readonly GanttStep[]) {\n const tasks = new Set<string>()\n const links = new Set<string>()\n for (const step of steps) {\n if (step.type === 'addTask') tasks.add(step.task.id)\n else if (step.type === 'addLink') links.add(step.link.id)\n else if (step.type === 'removeTask' || step.type === 'updateTask') tasks.add(step.id)\n else links.add(step.id)\n }\n return { tasks, links }\n}\n\n/** Gives the tasks a transaction added without an `order` key one, where they sort now. */\nfunction orderAdded(tr: GanttTransaction) {\n for (const step of tr.steps.slice()) {\n if (step.type !== 'addTask') continue\n const task = tr.getTask(step.task.id)\n if (!task || task.order != null) continue\n const parent = task.parent != null && tr.getTask(task.parent) ? task.parent : undefined\n const siblings = buildTree(tr.doc.tasks).childrenOf.get(parent) ?? []\n tr.moveTask(\n task.id,\n parent,\n siblings.findIndex((item) => item.id === task.id),\n )\n }\n}\n\n/**\n * Edit a Gantt chart together through a Yjs document. The tasks and links\n * live in two `Y.Map`s: the chart's `v-model` only mirrors them, and\n * changes made to `v-model` from outside are not written back. Undo only\n * takes back this user's own changes. Folding a summary stays local.\n */\nexport function collaboration(options: CollaborationOptions): GanttExtension {\n const { tasks, links } = options\n const ydoc = tasks.doc\n if (!ydoc || links.doc !== ydoc)\n throw new Error('[@dou-ui/gantt] tasks and links must be maps of the same Y.Doc')\n const { awareness, user } = options\n if (awareness && user) {\n const color = user.color ?? collaborationColors[ydoc.clientID % collaborationColors.length]\n awareness.setLocalStateField('user', { name: user.name, color })\n }\n\n /** This chart's own changes, which undo can take back. */\n const tracked = { gantt: 'tracked' }\n /** Changes that undo leaves alone, e.g. filling the maps. */\n const untracked = { gantt: 'untracked' }\n const undoManager = new Y.UndoManager([tasks, links], {\n trackedOrigins: new Set([tracked]),\n captureTimeout: 500,\n })\n /** Set while shared changes are applied to the chart, so they aren't written back. */\n let applying = false\n /** What undo or redo is running, to name the change. */\n let travelling: 'undo' | 'redo' | null = null\n let lastGroup: string | undefined\n\n function writeChanges(tr: GanttTransaction) {\n const { tasks: taskIds, links: linkIds } = touched(tr.steps)\n const taskById = new Map(tr.doc.tasks.map((task) => [task.id, task]))\n const linkById = new Map(tr.doc.links.map((link) => [link.id, link]))\n const tracking = tr.meta.history !== false\n // like the chart's own history: only changes in one group undo together\n if (tracking && (!tr.meta.group || tr.meta.group !== lastGroup)) undoManager.stopCapturing()\n if (tracking) lastGroup = tr.meta.group\n ydoc!.transact(\n () => {\n for (const id of taskIds) {\n const task = taskById.get(id)\n if (task) write(tasks, task)\n else tasks.delete(id)\n }\n for (const id of linkIds) {\n const link = linkById.get(id)\n if (link) write(links, link)\n else links.delete(id)\n }\n },\n tracking ? tracked : untracked,\n )\n }\n\n /** Adds steps to `tr` that bring the given tasks and links in line with the maps. */\n function pull(tr: GanttTransaction, taskIds: Iterable<string>, linkIds: Iterable<string>) {\n for (const id of taskIds) {\n const shared = read<GanttTask>(tasks, id)\n const local = tr.getTask(id)\n if (!shared) {\n if (local) tr.step({ type: 'removeTask', id })\n } else if (!local) tr.step({ type: 'addTask', task: shared })\n else {\n const patch = patchFor(local, shared)\n if (patch) tr.step({ type: 'updateTask', id, patch })\n }\n }\n for (const id of linkIds) {\n const shared = read<GanttLink>(links, id)\n const local = tr.getLink(id)\n if (!shared) {\n if (local) tr.step({ type: 'removeLink', id })\n } else if (!local) tr.step({ type: 'addLink', link: shared })\n else {\n const patch = patchFor(local, shared)\n if (patch) tr.step({ type: 'updateLink', id, patch })\n }\n }\n }\n\n return defineGanttExtension({\n name: 'collaboration',\n // after the other extensions, so it writes what they added\n priority: 0,\n transaction(tr) {\n if (applying) return\n orderAdded(tr)\n writeChanges(tr)\n },\n history: () => ({\n undo: () => run('undo'),\n redo: () => run('redo'),\n canUndo: () => undoManager.undoStack.length > 0,\n canRedo: () => undoManager.redoStack.length > 0,\n }),\n layers: awareness ? { above: [createPeers(awareness)] } : undefined,\n setup(context) {\n const apply = (fn: (tr: GanttTransaction) => void, origin: string) => {\n applying = true\n try {\n context.transact(fn, { history: false, schedule: false, origin })\n } finally {\n applying = false\n }\n }\n const current = (): GanttDocument => ({\n version: 1,\n tasks: toRaw(context.tasks.value),\n links: toRaw(context.links.value),\n })\n\n // the first one to open the plan brings its tasks; everyone else reads the maps\n if (!tasks.size && !links.size)\n ydoc.transact(() => fillMaps({ tasks, links }, current()), untracked)\n const doc = current()\n apply(\n (tr) =>\n pull(\n tr,\n new Set([...doc.tasks.map((task) => task.id), ...tasks.keys()]),\n new Set([...doc.links.map((link) => link.id), ...links.keys()]),\n ),\n 'remote',\n )\n\n // changes from others, and from undo: gathered per Yjs transaction\n const pending = { tasks: new Set<string>(), links: new Set<string>() }\n const gather =\n (map: Shared, into: Set<string>) =>\n (events: Y.YEvent<Y.Map<unknown>>[], transaction: Y.Transaction) => {\n if (transaction.origin === tracked || transaction.origin === untracked) return\n for (const event of events) {\n if (event.target === map) for (const key of event.changes.keys.keys()) into.add(key)\n else if (event.path.length) into.add(String(event.path[0]))\n }\n }\n const onTasks = gather(tasks, pending.tasks)\n const onLinks = gather(links, pending.links)\n const flush = () => {\n if (!pending.tasks.size && !pending.links.size) return\n const taskIds = [...pending.tasks]\n const linkIds = [...pending.links]\n pending.tasks.clear()\n pending.links.clear()\n apply((tr) => pull(tr, taskIds, linkIds), travelling ?? 'remote')\n }\n tasks.observeDeep(onTasks)\n links.observeDeep(onLinks)\n ydoc.on('afterTransaction', flush)\n\n let stopPresence: (() => void) | undefined\n if (awareness) {\n // what this user is dragging and has selected, for the others to see\n stopPresence = watch(\n [context.preview, context.selection],\n ([preview, selection]) => {\n const moving: GanttPresence['moving'] = []\n for (const [id, patch] of preview ?? []) {\n if (moving.length >= 100) break\n moving.push({ id, start: patch.start, end: patch.end })\n }\n awareness.setLocalStateField('gantt', {\n moving,\n selection: selection.slice(0, 100),\n } satisfies GanttPresence)\n },\n { immediate: true },\n )\n }\n\n return () => {\n tasks.unobserveDeep(onTasks)\n links.unobserveDeep(onLinks)\n ydoc.off('afterTransaction', flush)\n stopPresence?.()\n awareness?.setLocalStateField('gantt', null)\n }\n },\n })\n\n function run(action: 'undo' | 'redo') {\n const stack = action === 'undo' ? undoManager.undoStack : undoManager.redoStack\n if (!stack.length) return false\n travelling = action\n try {\n undoManager[action]()\n } finally {\n travelling = null\n }\n return true\n }\n}\n\nexport type { AwarenessLike, GanttPresence }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useGantt } from "../context.js";
|
|
2
|
+
import { spanOf } from "../core/model.js";
|
|
3
|
+
import { computed, defineComponent, h, onBeforeUnmount, shallowRef } from "vue";
|
|
4
|
+
// src/collaboration/peers.ts
|
|
5
|
+
/** A layer that outlines the tasks other people are dragging or have selected, in their colors. */
|
|
6
|
+
function createPeers(awareness) {
|
|
7
|
+
return defineComponent({
|
|
8
|
+
name: "DouGanttPeers",
|
|
9
|
+
setup() {
|
|
10
|
+
const ctx = useGantt();
|
|
11
|
+
const states = shallowRef([]);
|
|
12
|
+
const read = () => {
|
|
13
|
+
const out = [];
|
|
14
|
+
awareness.getStates().forEach((state, client) => {
|
|
15
|
+
if (client === awareness.clientID) return;
|
|
16
|
+
const user = state.user;
|
|
17
|
+
const presence = state.gantt;
|
|
18
|
+
if (!user || !presence) return;
|
|
19
|
+
out.push({
|
|
20
|
+
client,
|
|
21
|
+
name: user.name ?? "",
|
|
22
|
+
color: user.color ?? "currentColor",
|
|
23
|
+
presence
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
states.value = out;
|
|
27
|
+
};
|
|
28
|
+
read();
|
|
29
|
+
awareness.on("change", read);
|
|
30
|
+
onBeforeUnmount(() => awareness.off("change", read));
|
|
31
|
+
const rowById = computed(() => new Map(ctx.rows.value.map((row) => [row.task.id, row])));
|
|
32
|
+
/** Where a task's bar is, with `patch` applied: in timeline px. */
|
|
33
|
+
function box(id, patch) {
|
|
34
|
+
const row = rowById.value.get(id);
|
|
35
|
+
if (!row) return null;
|
|
36
|
+
const span = patch ? spanOf({
|
|
37
|
+
...row.task,
|
|
38
|
+
...patch
|
|
39
|
+
}) : row.span;
|
|
40
|
+
const top = ctx.rowTop(id);
|
|
41
|
+
if (!span || top == null) return null;
|
|
42
|
+
const left = ctx.xOf(span.start);
|
|
43
|
+
return {
|
|
44
|
+
left,
|
|
45
|
+
width: Math.max(2, ctx.xOf(span.end) - left),
|
|
46
|
+
top,
|
|
47
|
+
milestone: row.milestone
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return () => states.value.flatMap((peer) => {
|
|
51
|
+
const style = { "--dou-gantt-peer-color": peer.color };
|
|
52
|
+
const moving = new Set(peer.presence.moving.map((item) => item.id));
|
|
53
|
+
return [...peer.presence.moving.map((item) => ({
|
|
54
|
+
...box(item.id, item),
|
|
55
|
+
kind: "moving"
|
|
56
|
+
})), ...peer.presence.selection.filter((id) => !moving.has(id)).map((id) => ({
|
|
57
|
+
...box(id),
|
|
58
|
+
kind: "selected"
|
|
59
|
+
}))].filter((item) => Number.isFinite(item.left)).map((item, i) => h("div", {
|
|
60
|
+
key: `${peer.client}-${item.kind}-${i}`,
|
|
61
|
+
class: [
|
|
62
|
+
"dou-gantt__peer",
|
|
63
|
+
`is-${item.kind}`,
|
|
64
|
+
{ "is-milestone": item.milestone }
|
|
65
|
+
],
|
|
66
|
+
style: {
|
|
67
|
+
...style,
|
|
68
|
+
transform: `translate(${item.left}px, ${item.top}px)${item.milestone ? " rotate(45deg)" : ""}`,
|
|
69
|
+
width: item.milestone ? void 0 : `${item.width}px`
|
|
70
|
+
}
|
|
71
|
+
}, i === 0 ? h("span", { class: "dou-gantt__peer-name" }, peer.name) : void 0));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export { createPeers };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=peers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peers.js","names":[],"sources":["../../src/collaboration/peers.ts"],"sourcesContent":["import { computed, defineComponent, h, onBeforeUnmount, shallowRef, type Component } from 'vue'\nimport { useGantt } from '../context'\nimport { spanOf } from '../core/model'\n\n/** The part of an `Awareness` (`y-protocols/awareness`) the chart uses. */\nexport interface AwarenessLike {\n clientID: number\n getLocalState(): Record<string, unknown> | null\n setLocalStateField(field: string, value: unknown): void\n getStates(): Map<number, Record<string, unknown>>\n on(event: 'change', listener: (...args: unknown[]) => void): void\n off(event: 'change', listener: (...args: unknown[]) => void): void\n}\n\n/** What each person shares under the awareness field `gantt`. */\nexport interface GanttPresence {\n /** The tasks they are dragging, with the dates they would get. */\n moving: { id: string; start?: string; end?: string }[]\n selection: string[]\n}\n\ninterface Peer {\n client: number\n name: string\n color: string\n presence: GanttPresence\n}\n\n/** A layer that outlines the tasks other people are dragging or have selected, in their colors. */\nexport function createPeers(awareness: AwarenessLike): Component {\n return defineComponent({\n name: 'DouGanttPeers',\n setup() {\n const ctx = useGantt()\n const states = shallowRef<Peer[]>([])\n const read = () => {\n const out: Peer[] = []\n awareness.getStates().forEach((state, client) => {\n if (client === awareness.clientID) return\n const user = state.user as { name?: string; color?: string } | undefined\n const presence = state.gantt as GanttPresence | null | undefined\n if (!user || !presence) return\n out.push({ client, name: user.name ?? '', color: user.color ?? 'currentColor', presence })\n })\n states.value = out\n }\n read()\n awareness.on('change', read)\n onBeforeUnmount(() => awareness.off('change', read))\n\n const rowById = computed(() => new Map(ctx.rows.value.map((row) => [row.task.id, row])))\n\n /** Where a task's bar is, with `patch` applied: in timeline px. */\n function box(id: string, patch?: { start?: string; end?: string }) {\n const row = rowById.value.get(id)\n if (!row) return null\n const span = patch ? spanOf({ ...row.task, ...patch }) : row.span\n const top = ctx.rowTop(id)\n if (!span || top == null) return null\n const left = ctx.xOf(span.start)!\n return {\n left,\n width: Math.max(2, ctx.xOf(span.end)! - left),\n top,\n milestone: row.milestone,\n }\n }\n\n return () =>\n states.value.flatMap((peer) => {\n const style = { '--dou-gantt-peer-color': peer.color }\n const moving = new Set(peer.presence.moving.map((item) => item.id))\n const outlines = [\n ...peer.presence.moving.map((item) => ({ ...box(item.id, item), kind: 'moving' })),\n ...peer.presence.selection\n .filter((id) => !moving.has(id))\n .map((id) => ({ ...box(id), kind: 'selected' })),\n ].filter((item): item is NonNullable<ReturnType<typeof box>> & { kind: string } =>\n Number.isFinite(item.left),\n )\n return outlines.map((item, i) =>\n h(\n 'div',\n {\n key: `${peer.client}-${item.kind}-${i}`,\n class: ['dou-gantt__peer', `is-${item.kind}`, { 'is-milestone': item.milestone }],\n style: {\n ...style,\n transform: `translate(${item.left}px, ${item.top}px)${item.milestone ? ' rotate(45deg)' : ''}`,\n width: item.milestone ? undefined : `${item.width}px`,\n },\n },\n // the name, once per person\n i === 0 ? h('span', { class: 'dou-gantt__peer-name' }, peer.name) : undefined,\n ),\n )\n })\n },\n })\n}\n"],"mappings":";;;;;AA6BA,SAAgB,YAAY,WAAqC;CAC/D,OAAO,gBAAgB;EACrB,MAAM;EACN,QAAQ;GACN,MAAM,MAAM,SAAS;GACrB,MAAM,SAAS,WAAmB,CAAC,CAAC;GACpC,MAAM,aAAa;IACjB,MAAM,MAAc,CAAC;IACrB,UAAU,UAAU,CAAC,CAAC,SAAS,OAAO,WAAW;KAC/C,IAAI,WAAW,UAAU,UAAU;KACnC,MAAM,OAAO,MAAM;KACnB,MAAM,WAAW,MAAM;KACvB,IAAI,CAAC,QAAQ,CAAC,UAAU;KACxB,IAAI,KAAK;MAAE;MAAQ,MAAM,KAAK,QAAQ;MAAI,OAAO,KAAK,SAAS;MAAgB;KAAS,CAAC;IAC3F,CAAC;IACD,OAAO,QAAQ;GACjB;GACA,KAAK;GACL,UAAU,GAAG,UAAU,IAAI;GAC3B,sBAAsB,UAAU,IAAI,UAAU,IAAI,CAAC;GAEnD,MAAM,UAAU,eAAe,IAAI,IAAI,IAAI,KAAK,MAAM,KAAK,QAAQ,CAAC,IAAI,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC;;GAGvF,SAAS,IAAI,IAAY,OAA0C;IACjE,MAAM,MAAM,QAAQ,MAAM,IAAI,EAAE;IAChC,IAAI,CAAC,KAAK,OAAO;IACjB,MAAM,OAAO,QAAQ,OAAO;KAAE,GAAG,IAAI;KAAM,GAAG;IAAM,CAAC,IAAI,IAAI;IAC7D,MAAM,MAAM,IAAI,OAAO,EAAE;IACzB,IAAI,CAAC,QAAQ,OAAO,MAAM,OAAO;IACjC,MAAM,OAAO,IAAI,IAAI,KAAK,KAAK;IAC/B,OAAO;KACL;KACA,OAAO,KAAK,IAAI,GAAG,IAAI,IAAI,KAAK,GAAG,IAAK,IAAI;KAC5C;KACA,WAAW,IAAI;IACjB;GACF;GAEA,aACE,OAAO,MAAM,SAAS,SAAS;IAC7B,MAAM,QAAQ,EAAE,0BAA0B,KAAK,MAAM;IACrD,MAAM,SAAS,IAAI,IAAI,KAAK,SAAS,OAAO,KAAK,SAAS,KAAK,EAAE,CAAC;IASlE,OARiB,CACf,GAAG,KAAK,SAAS,OAAO,KAAK,UAAU;KAAE,GAAG,IAAI,KAAK,IAAI,IAAI;KAAG,MAAM;IAAS,EAAE,GACjF,GAAG,KAAK,SAAS,UACd,QAAQ,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAC/B,KAAK,QAAQ;KAAE,GAAG,IAAI,EAAE;KAAG,MAAM;IAAW,EAAE,CACnD,CAAC,CAAC,QAAQ,SACR,OAAO,SAAS,KAAK,IAAI,CAEpB,CAAA,CAAS,KAAK,MAAM,MACzB,EACE,OACA;KACE,KAAK,GAAG,KAAK,OAAO,GAAG,KAAK,KAAK,GAAG;KACpC,OAAO;MAAC;MAAmB,MAAM,KAAK;MAAQ,EAAE,gBAAgB,KAAK,UAAU;KAAC;KAChF,OAAO;MACL,GAAG;MACH,WAAW,aAAa,KAAK,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK,YAAY,mBAAmB;MAC1F,OAAO,KAAK,YAAY,KAAA,IAAY,GAAG,KAAK,MAAM;KACpD;IACF,GAEA,MAAM,IAAI,EAAE,QAAQ,EAAE,OAAO,uBAAuB,GAAG,KAAK,IAAI,IAAI,KAAA,CACtE,CACF;GACF,CAAC;EACL;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import cell_editor_vue_vue_type_script_setup_true_lang_default from "./cell-editor.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/components/cell-editor.vue
|
|
3
|
+
var cell_editor_default = cell_editor_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { cell_editor_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=cell-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell-editor.js","names":["_sfc_main"],"sources":["../../src/components/cell-editor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouInputDate } from 'dou-ui'\nimport { nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue'\n\n/*\n * The editor inside a table cell. It keeps its own value and hands it\n * back once: on Enter, on a pick from the calendar, or on a press outside.\n * Escape hands back nothing. A value `check` turns down keeps the field\n * open on Enter, marked invalid, and is dropped on a press outside.\n */\n\nconst props = defineProps<{\n kind: 'text' | 'number' | 'date' | 'datetime'\n value: string\n label: string\n placeholder?: string\n /** Keep editing after Enter, with the field emptied: for adding tasks one after another. */\n repeat?: boolean\n /** Whether a value can be saved. */\n check?: (value: string) => boolean\n}>()\nconst emit = defineEmits<{\n /** `null` when cancelled. */\n done: [value: string | null]\n /** Tab or Shift+Tab in a text field. */\n tab: [back: boolean]\n /** A value `check` turned down. */\n invalid: [value: string]\n}>()\n\nconst draft = ref(props.value)\nconst invalid = ref(false)\nconst root = useTemplateRef<HTMLElement>('root')\nlet finished = false\n\nfunction finish(value: string | null, outside = false) {\n if (finished) return\n if (value != null && props.check && !props.check(value)) {\n invalid.value = true\n emit('invalid', value)\n if (!outside) return\n value = null\n }\n if (props.repeat && value != null) {\n emit('done', value)\n draft.value = ''\n return\n }\n finished = true\n emit('done', value)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n if (event.key === 'Enter') finish(String(draft.value))\n else if (event.key === 'Escape') finish(null)\n else if (event.key === 'Tab' && props.kind === 'text') emit('tab', event.shiftKey)\n else return\n event.preventDefault()\n // keep the chart from reading the key as its own\n event.stopPropagation()\n}\n\n// a press outside the cell ends editing; the calendar is in its own layer\nfunction onOutside(event: PointerEvent) {\n const target = event.target as Element | null\n if (!target || root.value?.contains(target) || target.closest('.dou-floating')) return\n finish(String(draft.value), true)\n}\n\nfunction onPick(value: unknown) {\n if (typeof value !== 'string') return\n draft.value = value\n}\n\nonMounted(() => {\n document.addEventListener('pointerdown', onOutside, true)\n void nextTick(() => {\n const input = root.value?.querySelector<HTMLElement>(\n 'input, [tabindex=\"0\"], [role=\"spinbutton\"]',\n )\n input?.focus()\n if (input instanceof HTMLInputElement) input.select()\n })\n})\nonBeforeUnmount(() => document.removeEventListener('pointerdown', onOutside, true))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"dou-gantt__editor\"\n :class=\"`is-${kind}`\"\n @keydown=\"onKeydown\"\n @pointerdown.stop\n @dblclick.stop\n @click.stop\n >\n <DouInputDate\n v-if=\"kind === 'date' || kind === 'datetime'\"\n :model-value=\"draft || null\"\n :type=\"kind\"\n size=\"sm\"\n picker\n :aria-label=\"label\"\n @update:model-value=\"onPick\"\n @change=\"onPick\"\n />\n <input\n v-else\n v-model=\"draft\"\n class=\"dou-gantt__editor-input\"\n :aria-invalid=\"invalid || undefined\"\n :type=\"kind === 'number' ? 'number' : 'text'\"\n :inputmode=\"kind === 'number' ? 'decimal' : undefined\"\n :aria-label=\"label\"\n :placeholder=\"placeholder\"\n @input=\"invalid = false\"\n />\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DouInputDate } from "dou-ui";
|
|
2
|
+
import { createBlock, createElementBlock, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, unref, useTemplateRef, vModelDynamic, withDirectives, withModifiers } from "vue";
|
|
3
|
+
// src/components/cell-editor.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
+
var _hoisted_1 = [
|
|
5
|
+
"aria-invalid",
|
|
6
|
+
"type",
|
|
7
|
+
"inputmode",
|
|
8
|
+
"aria-label",
|
|
9
|
+
"placeholder"
|
|
10
|
+
];
|
|
11
|
+
var cell_editor_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
12
|
+
__name: "cell-editor",
|
|
13
|
+
props: {
|
|
14
|
+
kind: {},
|
|
15
|
+
value: {},
|
|
16
|
+
label: {},
|
|
17
|
+
placeholder: {},
|
|
18
|
+
repeat: { type: Boolean },
|
|
19
|
+
check: { type: Function }
|
|
20
|
+
},
|
|
21
|
+
emits: [
|
|
22
|
+
"done",
|
|
23
|
+
"tab",
|
|
24
|
+
"invalid"
|
|
25
|
+
],
|
|
26
|
+
setup(__props, { emit: __emit }) {
|
|
27
|
+
const props = __props;
|
|
28
|
+
const emit = __emit;
|
|
29
|
+
const draft = ref(props.value);
|
|
30
|
+
const invalid = ref(false);
|
|
31
|
+
const root = useTemplateRef("root");
|
|
32
|
+
let finished = false;
|
|
33
|
+
function finish(value, outside = false) {
|
|
34
|
+
if (finished) return;
|
|
35
|
+
if (value != null && props.check && !props.check(value)) {
|
|
36
|
+
invalid.value = true;
|
|
37
|
+
emit("invalid", value);
|
|
38
|
+
if (!outside) return;
|
|
39
|
+
value = null;
|
|
40
|
+
}
|
|
41
|
+
if (props.repeat && value != null) {
|
|
42
|
+
emit("done", value);
|
|
43
|
+
draft.value = "";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
finished = true;
|
|
47
|
+
emit("done", value);
|
|
48
|
+
}
|
|
49
|
+
function onKeydown(event) {
|
|
50
|
+
if (event.isComposing) return;
|
|
51
|
+
if (event.key === "Enter") finish(String(draft.value));
|
|
52
|
+
else if (event.key === "Escape") finish(null);
|
|
53
|
+
else if (event.key === "Tab" && props.kind === "text") emit("tab", event.shiftKey);
|
|
54
|
+
else return;
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
}
|
|
58
|
+
function onOutside(event) {
|
|
59
|
+
const target = event.target;
|
|
60
|
+
if (!target || root.value?.contains(target) || target.closest(".dou-floating")) return;
|
|
61
|
+
finish(String(draft.value), true);
|
|
62
|
+
}
|
|
63
|
+
function onPick(value) {
|
|
64
|
+
if (typeof value !== "string") return;
|
|
65
|
+
draft.value = value;
|
|
66
|
+
}
|
|
67
|
+
onMounted(() => {
|
|
68
|
+
document.addEventListener("pointerdown", onOutside, true);
|
|
69
|
+
nextTick(() => {
|
|
70
|
+
const input = root.value?.querySelector("input, [tabindex=\"0\"], [role=\"spinbutton\"]");
|
|
71
|
+
input?.focus();
|
|
72
|
+
if (input instanceof HTMLInputElement) input.select();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
onBeforeUnmount(() => document.removeEventListener("pointerdown", onOutside, true));
|
|
76
|
+
return (_ctx, _cache) => {
|
|
77
|
+
return openBlock(), createElementBlock("div", {
|
|
78
|
+
ref_key: "root",
|
|
79
|
+
ref: root,
|
|
80
|
+
class: normalizeClass(["dou-gantt__editor", `is-${__props.kind}`]),
|
|
81
|
+
onKeydown,
|
|
82
|
+
onPointerdown: _cache[2] || (_cache[2] = withModifiers(() => {}, ["stop"])),
|
|
83
|
+
onDblclick: _cache[3] || (_cache[3] = withModifiers(() => {}, ["stop"])),
|
|
84
|
+
onClick: _cache[4] || (_cache[4] = withModifiers(() => {}, ["stop"]))
|
|
85
|
+
}, [__props.kind === "date" || __props.kind === "datetime" ? (openBlock(), createBlock(unref(DouInputDate), {
|
|
86
|
+
key: 0,
|
|
87
|
+
"model-value": draft.value || null,
|
|
88
|
+
type: __props.kind,
|
|
89
|
+
size: "sm",
|
|
90
|
+
picker: "",
|
|
91
|
+
"aria-label": __props.label,
|
|
92
|
+
"onUpdate:modelValue": onPick,
|
|
93
|
+
onChange: onPick
|
|
94
|
+
}, null, 8, [
|
|
95
|
+
"model-value",
|
|
96
|
+
"type",
|
|
97
|
+
"aria-label"
|
|
98
|
+
])) : withDirectives((openBlock(), createElementBlock("input", {
|
|
99
|
+
key: 1,
|
|
100
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => draft.value = $event),
|
|
101
|
+
class: "dou-gantt__editor-input",
|
|
102
|
+
"aria-invalid": invalid.value || void 0,
|
|
103
|
+
type: __props.kind === "number" ? "number" : "text",
|
|
104
|
+
inputmode: __props.kind === "number" ? "decimal" : void 0,
|
|
105
|
+
"aria-label": __props.label,
|
|
106
|
+
placeholder: __props.placeholder,
|
|
107
|
+
onInput: _cache[1] || (_cache[1] = ($event) => invalid.value = false)
|
|
108
|
+
}, null, 40, _hoisted_1)), [[vModelDynamic, draft.value]])], 34);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
export { cell_editor_vue_vue_type_script_setup_true_lang_default as default };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=cell-editor.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell-editor.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_withModifiers","_createBlock","_unref","_withDirectives","_vModelDynamic"],"sources":["../../src/components/cell-editor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouInputDate } from 'dou-ui'\nimport { nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue'\n\n/*\n * The editor inside a table cell. It keeps its own value and hands it\n * back once: on Enter, on a pick from the calendar, or on a press outside.\n * Escape hands back nothing. A value `check` turns down keeps the field\n * open on Enter, marked invalid, and is dropped on a press outside.\n */\n\nconst props = defineProps<{\n kind: 'text' | 'number' | 'date' | 'datetime'\n value: string\n label: string\n placeholder?: string\n /** Keep editing after Enter, with the field emptied: for adding tasks one after another. */\n repeat?: boolean\n /** Whether a value can be saved. */\n check?: (value: string) => boolean\n}>()\nconst emit = defineEmits<{\n /** `null` when cancelled. */\n done: [value: string | null]\n /** Tab or Shift+Tab in a text field. */\n tab: [back: boolean]\n /** A value `check` turned down. */\n invalid: [value: string]\n}>()\n\nconst draft = ref(props.value)\nconst invalid = ref(false)\nconst root = useTemplateRef<HTMLElement>('root')\nlet finished = false\n\nfunction finish(value: string | null, outside = false) {\n if (finished) return\n if (value != null && props.check && !props.check(value)) {\n invalid.value = true\n emit('invalid', value)\n if (!outside) return\n value = null\n }\n if (props.repeat && value != null) {\n emit('done', value)\n draft.value = ''\n return\n }\n finished = true\n emit('done', value)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.isComposing) return\n if (event.key === 'Enter') finish(String(draft.value))\n else if (event.key === 'Escape') finish(null)\n else if (event.key === 'Tab' && props.kind === 'text') emit('tab', event.shiftKey)\n else return\n event.preventDefault()\n // keep the chart from reading the key as its own\n event.stopPropagation()\n}\n\n// a press outside the cell ends editing; the calendar is in its own layer\nfunction onOutside(event: PointerEvent) {\n const target = event.target as Element | null\n if (!target || root.value?.contains(target) || target.closest('.dou-floating')) return\n finish(String(draft.value), true)\n}\n\nfunction onPick(value: unknown) {\n if (typeof value !== 'string') return\n draft.value = value\n}\n\nonMounted(() => {\n document.addEventListener('pointerdown', onOutside, true)\n void nextTick(() => {\n const input = root.value?.querySelector<HTMLElement>(\n 'input, [tabindex=\"0\"], [role=\"spinbutton\"]',\n )\n input?.focus()\n if (input instanceof HTMLInputElement) input.select()\n })\n})\nonBeforeUnmount(() => document.removeEventListener('pointerdown', onOutside, true))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"dou-gantt__editor\"\n :class=\"`is-${kind}`\"\n @keydown=\"onKeydown\"\n @pointerdown.stop\n @dblclick.stop\n @click.stop\n >\n <DouInputDate\n v-if=\"kind === 'date' || kind === 'datetime'\"\n :model-value=\"draft || null\"\n :type=\"kind\"\n size=\"sm\"\n picker\n :aria-label=\"label\"\n @update:model-value=\"onPick\"\n @change=\"onPick\"\n />\n <input\n v-else\n v-model=\"draft\"\n class=\"dou-gantt__editor-input\"\n :aria-invalid=\"invalid || undefined\"\n :type=\"kind === 'number' ? 'number' : 'text'\"\n :inputmode=\"kind === 'number' ? 'decimal' : undefined\"\n :aria-label=\"label\"\n :placeholder=\"placeholder\"\n @input=\"invalid = false\"\n />\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;EAWA,MAAM,QAAQ;EAUd,MAAM,OAAO;EASb,MAAM,QAAQ,IAAI,MAAM,KAAK;EAC7B,MAAM,UAAU,IAAI,KAAK;EACzB,MAAM,OAAO,eAA4B,MAAM;EAC/C,IAAI,WAAW;EAEf,SAAS,OAAO,OAAsB,UAAU,OAAO;GACrD,IAAI,UAAU;GACd,IAAI,SAAS,QAAQ,MAAM,SAAS,CAAC,MAAM,MAAM,KAAK,GAAG;IACvD,QAAQ,QAAQ;IAChB,KAAK,WAAW,KAAK;IACrB,IAAI,CAAC,SAAS;IACd,QAAQ;GACV;GACA,IAAI,MAAM,UAAU,SAAS,MAAM;IACjC,KAAK,QAAQ,KAAK;IAClB,MAAM,QAAQ;IACd;GACF;GACA,WAAW;GACX,KAAK,QAAQ,KAAK;EACpB;EAEA,SAAS,UAAU,OAAsB;GACvC,IAAI,MAAM,aAAa;GACvB,IAAI,MAAM,QAAQ,SAAS,OAAO,OAAO,MAAM,KAAK,CAAC;QAChD,IAAI,MAAM,QAAQ,UAAU,OAAO,IAAI;QACvC,IAAI,MAAM,QAAQ,SAAS,MAAM,SAAS,QAAQ,KAAK,OAAO,MAAM,QAAQ;QAC5E;GACL,MAAM,eAAe;GAErB,MAAM,gBAAgB;EACxB;EAGA,SAAS,UAAU,OAAqB;GACtC,MAAM,SAAS,MAAM;GACrB,IAAI,CAAC,UAAU,KAAK,OAAO,SAAS,MAAM,KAAK,OAAO,QAAQ,eAAe,GAAG;GAChF,OAAO,OAAO,MAAM,KAAK,GAAG,IAAI;EAClC;EAEA,SAAS,OAAO,OAAgB;GAC9B,IAAI,OAAO,UAAU,UAAU;GAC/B,MAAM,QAAQ;EAChB;EAEA,gBAAgB;GACd,SAAS,iBAAiB,eAAe,WAAW,IAAI;GACxD,eAAoB;IAClB,MAAM,QAAQ,KAAK,OAAO,cACxB,gDACF;IACA,OAAO,MAAM;IACb,IAAI,iBAAiB,kBAAkB,MAAM,OAAO;GACtD,CAAC;EACH,CAAC;EACD,sBAAsB,SAAS,oBAAoB,eAAe,WAAW,IAAI,CAAC;;GAIhF,OAAAA,UAAA,GAAAC,mBA8BM,OAAA;IA7BA,SAAA;IAAJ,KAAI;IACJ,OAAKC,eAAA,CAAC,qBAAmB,MACX,QAAA,MAAI,CAAA;IACR;IACT,eAAW,OAAA,OAAA,OAAA,KAAAC,oBAAZ,CAAA,GAAiB,CAAA,MAAA,CAAA;IAChB,YAAQ,OAAA,OAAA,OAAA,KAAAA,oBAAT,CAAA,GAAc,CAAA,MAAA,CAAA;IACb,SAAK,OAAA,OAAA,OAAA,KAAAA,oBAAN,CAAA,GAAW,CAAA,MAAA,CAAA;G,IAGH,QAAA,SAAI,UAAe,QAAA,SAAI,cAD/BH,UAAA,GAAAI,YASEC,MAAA,YAAA,GAAA;;IAPC,eAAa,MAAA,SAAK;IAClB,MAAM,QAAA;IACP,MAAK;IACL,QAAA;IACC,cAAY,QAAA;IACZ,uBAAoB;IACpB,UAAQ;;;;;G,MAEXC,gBAAAN,UAAA,GAAAC,mBAUE,SAAA;;IARS,uBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAK,QAAA;IACd,OAAM;IACL,gBAAc,QAAA,SAAW,KAAA;IACzB,MAAM,QAAA,SAAI,WAAA,WAAA;IACV,WAAW,QAAA,SAAI,WAAA,YAA4B,KAAA;IAC3C,cAAY,QAAA;IACZ,aAAa,QAAA;IACb,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,QAAO;G,4BAPN,CAAAM,eAAA,MAAA,KAAK,C"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import gantt_menu_vue_vue_type_script_setup_true_lang_default from "./gantt-menu.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/components/gantt-menu.vue
|
|
3
|
+
var gantt_menu_default = gantt_menu_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { gantt_menu_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=gantt-menu.js.map
|