@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/collaboration/index.js +323 -0
  4. package/dist/collaboration/index.js.map +1 -0
  5. package/dist/collaboration/peers.js +78 -0
  6. package/dist/collaboration/peers.js.map +1 -0
  7. package/dist/components/cell-editor.js +6 -0
  8. package/dist/components/cell-editor.js.map +1 -0
  9. package/dist/components/cell-editor.vue_vue_type_script_setup_true_lang.js +114 -0
  10. package/dist/components/cell-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
  11. package/dist/components/gantt-menu.js +6 -0
  12. package/dist/components/gantt-menu.js.map +1 -0
  13. package/dist/components/gantt-menu.vue_vue_type_script_setup_true_lang.js +40 -0
  14. package/dist/components/gantt-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  15. package/dist/components/time-grid.js +6 -0
  16. package/dist/components/time-grid.js.map +1 -0
  17. package/dist/components/time-grid.vue_vue_type_script_setup_true_lang.js +32 -0
  18. package/dist/components/time-grid.vue_vue_type_script_setup_true_lang.js.map +1 -0
  19. package/dist/components/time-header.js +6 -0
  20. package/dist/components/time-header.js.map +1 -0
  21. package/dist/components/time-header.vue_vue_type_script_setup_true_lang.js +60 -0
  22. package/dist/components/time-header.vue_vue_type_script_setup_true_lang.js.map +1 -0
  23. package/dist/context.js +12 -0
  24. package/dist/context.js.map +1 -0
  25. package/dist/core/baseline.js +48 -0
  26. package/dist/core/baseline.js.map +1 -0
  27. package/dist/core/calendar.js +191 -0
  28. package/dist/core/calendar.js.map +1 -0
  29. package/dist/core/csv.js +424 -0
  30. package/dist/core/csv.js.map +1 -0
  31. package/dist/core/document.js +231 -0
  32. package/dist/core/document.js.map +1 -0
  33. package/dist/core/extension.js +42 -0
  34. package/dist/core/extension.js.map +1 -0
  35. package/dist/core/geometry.js +112 -0
  36. package/dist/core/geometry.js.map +1 -0
  37. package/dist/core/history.js +70 -0
  38. package/dist/core/history.js.map +1 -0
  39. package/dist/core/links.js +72 -0
  40. package/dist/core/links.js.map +1 -0
  41. package/dist/core/model.js +52 -0
  42. package/dist/core/model.js.map +1 -0
  43. package/dist/core/order.js +34 -0
  44. package/dist/core/order.js.map +1 -0
  45. package/dist/core/scale.js +118 -0
  46. package/dist/core/scale.js.map +1 -0
  47. package/dist/core/schedule.js +235 -0
  48. package/dist/core/schedule.js.map +1 -0
  49. package/dist/core/time.js +81 -0
  50. package/dist/core/time.js.map +1 -0
  51. package/dist/core/transaction.js +371 -0
  52. package/dist/core/transaction.js.map +1 -0
  53. package/dist/core/tree.js +114 -0
  54. package/dist/core/tree.js.map +1 -0
  55. package/dist/export/draw.js +214 -0
  56. package/dist/export/draw.js.map +1 -0
  57. package/dist/export/theme.js +131 -0
  58. package/dist/export/theme.js.map +1 -0
  59. package/dist/gantt.js +6 -0
  60. package/dist/gantt.js.map +1 -0
  61. package/dist/gantt.vue_vue_type_script_setup_true_lang.js +2774 -0
  62. package/dist/gantt.vue_vue_type_script_setup_true_lang.js.map +1 -0
  63. package/dist/index.js +20 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/labels.js +87 -0
  66. package/dist/labels.js.map +1 -0
  67. package/dist/locale.js +355 -0
  68. package/dist/locale.js.map +1 -0
  69. package/dist/scheduler/index.js +8 -0
  70. package/dist/scheduler/index.js.map +1 -0
  71. package/dist/scheduler/layout.js +97 -0
  72. package/dist/scheduler/layout.js.map +1 -0
  73. package/dist/scheduler/scheduler.js +6 -0
  74. package/dist/scheduler/scheduler.js.map +1 -0
  75. package/dist/scheduler/scheduler.vue_vue_type_script_setup_true_lang.js +1359 -0
  76. package/dist/scheduler/scheduler.vue_vue_type_script_setup_true_lang.js.map +1 -0
  77. package/dist/scheduler/style.d.ts +1 -0
  78. package/dist/scheduler/style.js +2 -0
  79. package/dist/style.css +1 -0
  80. package/dist/style.d.ts +1 -0
  81. package/dist/style.js +4 -0
  82. package/dist/time-axis.js +337 -0
  83. package/dist/time-axis.js.map +1 -0
  84. package/dist/types/collaboration/index.d.ts +39 -0
  85. package/dist/types/collaboration/peers.d.ts +22 -0
  86. package/dist/types/components/cell-editor.vue.d.ts +21 -0
  87. package/dist/types/components/gantt-menu.vue.d.ts +18 -0
  88. package/dist/types/components/time-grid.vue.d.ts +12 -0
  89. package/dist/types/components/time-header.vue.d.ts +23 -0
  90. package/dist/types/context.d.ts +64 -0
  91. package/dist/types/core/baseline.d.ts +14 -0
  92. package/dist/types/core/calendar.d.ts +75 -0
  93. package/dist/types/core/csv.d.ts +77 -0
  94. package/dist/types/core/document.d.ts +51 -0
  95. package/dist/types/core/extension.d.ts +88 -0
  96. package/dist/types/core/geometry.d.ts +33 -0
  97. package/dist/types/core/history.d.ts +19 -0
  98. package/dist/types/core/links.d.ts +20 -0
  99. package/dist/types/core/menu.d.ts +27 -0
  100. package/dist/types/core/model.d.ts +72 -0
  101. package/dist/types/core/order.d.ts +9 -0
  102. package/dist/types/core/scale.d.ts +33 -0
  103. package/dist/types/core/schedule.d.ts +44 -0
  104. package/dist/types/core/time.d.ts +39 -0
  105. package/dist/types/core/transaction.d.ts +134 -0
  106. package/dist/types/core/tree.d.ts +30 -0
  107. package/dist/types/export/draw.d.ts +76 -0
  108. package/dist/types/export/theme.d.ts +49 -0
  109. package/dist/types/gantt.vue.d.ts +54 -0
  110. package/dist/types/index.d.ts +148 -0
  111. package/dist/types/labels.d.ts +24 -0
  112. package/dist/types/locale.d.ts +161 -0
  113. package/dist/types/scheduler/index.d.ts +102 -0
  114. package/dist/types/scheduler/layout.d.ts +58 -0
  115. package/dist/types/scheduler/scheduler.vue.d.ts +46 -0
  116. package/dist/types/scheduler/types.d.ts +109 -0
  117. package/dist/types/time-axis.d.ts +80 -0
  118. package/dist/types/types.d.ts +305 -0
  119. package/package.json +79 -4
@@ -0,0 +1,2774 @@
1
+ import { ganttKey } from "./context.js";
2
+ import { resolveGanttExtensions } from "./core/extension.js";
3
+ import gantt_menu_default from "./components/gantt-menu.js";
4
+ import cell_editor_default from "./components/cell-editor.js";
5
+ import time_grid_default from "./components/time-grid.js";
6
+ import { MINUTES_PER_DAY, dayIndex, dayStart, formatTime, parseTime } from "./core/time.js";
7
+ import time_header_default from "./components/time-header.js";
8
+ import { endKeepingWork, isWorkTime, nextWorkTime, resolveCalendar, shiftWorkMinutes, shiftWorkdays, workdaysBetween, workingMinutes } from "./core/calendar.js";
9
+ import { arrowPath, roundedPath, routeLink } from "./core/geometry.js";
10
+ import { createId, spanOf, writeSpan } from "./core/model.js";
11
+ import { buildTree } from "./core/tree.js";
12
+ import { baselineSpans } from "./core/baseline.js";
13
+ import { DAY_WIDTH_RANGE, levelByName, snapStep } from "./core/scale.js";
14
+ import { format, useGanttMessages } from "./locale.js";
15
+ import { dateLabel, datesLabel, durationLabel, endLabel, markerRoom, spokenDuration } from "./labels.js";
16
+ import { drawGantt, paddedRange, svgDocument } from "./export/draw.js";
17
+ import { readTheme } from "./export/theme.js";
18
+ import { GanttTransaction, emptyDocument } from "./core/transaction.js";
19
+ import { GanttHistory } from "./core/history.js";
20
+ import { linkRefusal, linkTypeOf } from "./core/links.js";
21
+ import { readDocument } from "./core/document.js";
22
+ import { applyGanttCSV, readGanttCSV } from "./core/csv.js";
23
+ import { computeCriticalPath, computeSchedule, findBrokenLinks } from "./core/schedule.js";
24
+ import { useTimeAxis } from "./time-axis.js";
25
+ import { holdPointer, matchesHotkey, svgToPNG, useElementSize, useLocale } from "dou-ui";
26
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toRaw, unref, useId, useModel, useTemplateRef, watch, withCtx, withModifiers } from "vue";
27
+ import { ChevronDownIcon, ChevronRightIcon, EditIcon, IndentIcon, ListTreeIcon, OutdentIcon, PlusIcon, TrashIcon } from "@dou-ui/icons";
28
+ // src/gantt.vue?vue&type=script&setup=true&lang.ts
29
+ var _hoisted_1 = ["aria-label"];
30
+ var _hoisted_2 = {
31
+ class: "dou-gantt__sr-only",
32
+ "aria-live": "polite"
33
+ };
34
+ var _hoisted_3 = {
35
+ key: 0,
36
+ class: "dou-gantt__notice",
37
+ "aria-hidden": "true"
38
+ };
39
+ var _hoisted_4 = ["aria-rowcount"];
40
+ var _hoisted_5 = {
41
+ class: "dou-gantt__table-head",
42
+ role: "row"
43
+ };
44
+ var _hoisted_6 = [
45
+ "data-row",
46
+ "aria-level",
47
+ "aria-rowindex",
48
+ "aria-expanded",
49
+ "aria-selected",
50
+ "onPointerenter",
51
+ "onPointerdown",
52
+ "onClick",
53
+ "onDblclick",
54
+ "onContextmenu"
55
+ ];
56
+ var _hoisted_7 = ["onDblclick"];
57
+ var _hoisted_8 = ["aria-label", "onClick"];
58
+ var _hoisted_9 = {
59
+ key: 1,
60
+ class: "dou-gantt__toggle-space"
61
+ };
62
+ var _hoisted_10 = {
63
+ key: 2,
64
+ class: "dou-gantt__cell-text"
65
+ };
66
+ var _hoisted_11 = {
67
+ class: "dou-gantt__td is-title",
68
+ role: "gridcell"
69
+ };
70
+ var _hoisted_12 = ["title"];
71
+ var _hoisted_13 = {
72
+ key: 0,
73
+ class: "dou-gantt__marker-text"
74
+ };
75
+ var _hoisted_14 = {
76
+ key: 0,
77
+ class: "dou-gantt__row-titles"
78
+ };
79
+ var _hoisted_15 = ["width", "height"];
80
+ var _hoisted_16 = ["onClick", "onContextmenu"];
81
+ var _hoisted_17 = ["d"];
82
+ var _hoisted_18 = ["d"];
83
+ var _hoisted_19 = ["d"];
84
+ var _hoisted_20 = ["d"];
85
+ var _hoisted_21 = ["onPointerenter"];
86
+ var _hoisted_22 = [
87
+ "data-task",
88
+ "aria-label",
89
+ "onPointerdown",
90
+ "onClick",
91
+ "onDblclick",
92
+ "onContextmenu"
93
+ ];
94
+ var _hoisted_23 = {
95
+ key: 1,
96
+ class: "dou-gantt__bar-label"
97
+ };
98
+ var _hoisted_24 = ["onPointerdown"];
99
+ var _hoisted_25 = ["onPointerdown"];
100
+ var _hoisted_26 = ["onPointerdown"];
101
+ var _hoisted_27 = ["onPointerdown"];
102
+ var _hoisted_28 = ["onPointerdown"];
103
+ var _hoisted_29 = [
104
+ "aria-label",
105
+ "aria-valuenow",
106
+ "aria-valuemax"
107
+ ];
108
+ var HEADER = 56;
109
+ /** Room around the visible part that is rendered too, in px. */
110
+ var OVERSCAN_X = 320;
111
+ var OVERSCAN_ROWS = 6;
112
+ /** Below this width the table steps aside and the timeline takes the whole width. */
113
+ var NARROW = 520;
114
+ var BASELINE_SHIFT = 4;
115
+ /** The planned spans, by task. */
116
+ var MILESTONE = 7;
117
+ var TABLE_MIN = 160;
118
+ var FLEX_COLUMN = 64;
119
+ var TITLE_MIN = 120;
120
+ /** Movement in px before a press becomes a drag. */
121
+ var LONG_PRESS = 300;
122
+ /** How close to the edge a drag starts scrolling, in px. */
123
+ var EDGE = 32;
124
+ /**
125
+ * Where a menu opened, on the timeline: under the pointer when it is over
126
+ * the timeline, otherwise at the task's start.
127
+ */
128
+ var EXPORT_MAX = 16e3;
129
+ /** Room on a printed page, in px at 96 per inch: A4 less 10mm margins. */
130
+ var gantt_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
131
+ name: "DouGantt",
132
+ __name: "gantt",
133
+ props: /*@__PURE__*/ mergeModels({
134
+ calendar: {},
135
+ columns: {},
136
+ table: {
137
+ type: Boolean,
138
+ default: true
139
+ },
140
+ tableWidth: { default: 360 },
141
+ rowHeight: { default: 36 },
142
+ barLabel: { default: "auto" },
143
+ range: {},
144
+ today: {
145
+ type: Boolean,
146
+ default: true
147
+ },
148
+ weekStart: {},
149
+ scheduling: { default: "manual" },
150
+ addRow: {
151
+ type: Boolean,
152
+ default: true
153
+ },
154
+ menu: {
155
+ type: Boolean,
156
+ default: true
157
+ },
158
+ contextMenu: {},
159
+ critical: { type: Boolean },
160
+ readonly: { type: Boolean },
161
+ canLink: {},
162
+ canMove: {},
163
+ markers: {},
164
+ baseline: {},
165
+ extensions: {},
166
+ ariaLabel: {},
167
+ messages: {}
168
+ }, {
169
+ "modelValue": { default: emptyDocument },
170
+ "modelModifiers": {},
171
+ "selection": { default: () => [] },
172
+ "selectionModifiers": {},
173
+ "scale": {},
174
+ "scaleModifiers": {}
175
+ }),
176
+ emits: /*@__PURE__*/ mergeModels([
177
+ "transaction",
178
+ "transactionCancel",
179
+ "link",
180
+ "taskClick",
181
+ "taskDblclick",
182
+ "taskContextmenu",
183
+ "linkClick",
184
+ "linkContextmenu"
185
+ ], [
186
+ "update:modelValue",
187
+ "update:selection",
188
+ "update:scale"
189
+ ]),
190
+ setup(__props, { expose: __expose, emit: __emit }) {
191
+ const props = __props;
192
+ const model = useModel(__props, "modelValue");
193
+ /** The `v-model`, with a wrongly shaped one read as well as it can be. */
194
+ const source = computed(() => readDocument(model.value));
195
+ const tasks = computed(() => source.value.tasks);
196
+ const links = computed(() => source.value.links);
197
+ const selection = useModel(__props, "selection");
198
+ const scale = useModel(__props, "scale");
199
+ const emit = __emit;
200
+ const messages = useGanttMessages(() => props.messages);
201
+ const resolved = computed(() => resolveGanttExtensions(props.extensions));
202
+ const { locale } = useLocale();
203
+ const weekStart = computed(() => props.weekStart ?? locale.value.region.weekStart);
204
+ /** Height of the two-row time header, in px; the table's header matches it. */
205
+ const root = useTemplateRef("root");
206
+ const chart = useTemplateRef("chart");
207
+ const tableBody = useTemplateRef("tableBody");
208
+ const { width: rootWidth } = useElementSize(root);
209
+ const { width: viewWidth, height: viewHeight } = useElementSize(chart);
210
+ const history = new GanttHistory();
211
+ /** An extension's history, used instead of the chart's own. */
212
+ let customHistory = null;
213
+ const canUndo = ref(false);
214
+ const canRedo = ref(false);
215
+ const syncHistory = () => {
216
+ canUndo.value = customHistory ? customHistory.canUndo() : history.canUndo;
217
+ canRedo.value = customHistory ? customHistory.canRedo() : history.canRedo;
218
+ };
219
+ const editable = computed(() => !props.readonly);
220
+ let latest = null;
221
+ const docOf = () => latest ?? toRaw(source.value);
222
+ /** The last transaction committed, for reading out what an extension's undo changed. */
223
+ let lastCommit = null;
224
+ /** Runs the extensions' `transaction` hooks; `false` when one cancelled the change. */
225
+ function check(tr) {
226
+ for (const hook of resolved.value.transactions) {
227
+ hook(tr, context);
228
+ if (tr.cancelled) {
229
+ emit("transactionCancel", tr);
230
+ refuse(tr.cancelReason ?? messages.value.changeCancelled);
231
+ return false;
232
+ }
233
+ }
234
+ return true;
235
+ }
236
+ function commit(tr) {
237
+ if (!check(tr)) return false;
238
+ lastCommit = tr;
239
+ if (!latest) nextTick(() => latest = null);
240
+ latest = tr.doc;
241
+ model.value = tr.doc;
242
+ if (tr.touches("tasks")) {
243
+ const ids = new Set(tr.doc.tasks.map((task) => task.id));
244
+ const kept = selection.value.filter((id) => ids.has(id));
245
+ if (kept.length !== selection.value.length) selection.value = kept;
246
+ }
247
+ if (selectedLink.value && !tr.doc.links.some((link) => link.id === selectedLink.value)) selectedLink.value = null;
248
+ emit("transaction", tr);
249
+ return true;
250
+ }
251
+ /** Tasks a transaction gave dates to. */
252
+ function datedIn(steps) {
253
+ const out = /* @__PURE__ */ new Set();
254
+ for (const step of steps) if (step.type === "addTask") out.add(step.task.id);
255
+ else if (step.type === "updateTask" && ("start" in step.patch || "end" in step.patch)) out.add(step.id);
256
+ return out;
257
+ }
258
+ /** How many tasks automatic scheduling pushed in each transaction. */
259
+ const pushedBy = /* @__PURE__ */ new WeakMap();
260
+ /** With automatic scheduling, pushes the tasks that wait on what changed, in the same transaction. */
261
+ function pushFollowers(tr) {
262
+ if (props.scheduling !== "auto" || tr.meta.schedule === false) return;
263
+ const result = computeSchedule(tr.doc, {
264
+ calendar: calendar.value,
265
+ fixed: datedIn(tr.steps)
266
+ });
267
+ for (const [id, patch] of result.moves) tr.updateTask(id, patch);
268
+ if (result.moves.size) pushedBy.set(tr, result.moves.size);
269
+ }
270
+ /** Applies changes as one transaction, one undo step. */
271
+ function transact(fn, meta) {
272
+ const tr = new GanttTransaction(docOf());
273
+ if (meta) tr.setMeta(meta);
274
+ fn(tr);
275
+ if (!tr.changed) return tr;
276
+ pushFollowers(tr);
277
+ const before = selection.value;
278
+ if (!commit(tr)) return tr;
279
+ if (!customHistory) history.record(tr, before, selection.value);
280
+ syncHistory();
281
+ return tr;
282
+ }
283
+ function travel(direction) {
284
+ if (customHistory) {
285
+ const before = lastCommit;
286
+ const done = customHistory[direction]();
287
+ syncHistory();
288
+ return done && lastCommit !== before ? lastCommit : null;
289
+ }
290
+ const result = history[direction](docOf());
291
+ if (!result) return null;
292
+ result.tr.setMeta({
293
+ origin: direction,
294
+ history: false,
295
+ schedule: false
296
+ });
297
+ commit(result.tr);
298
+ selection.value = result.selection;
299
+ syncHistory();
300
+ return result.tr;
301
+ }
302
+ const undo = () => !!travel("undo");
303
+ const redo = () => !!travel("redo");
304
+ /** Moves tasks so that every link is kept, as one undo step. */
305
+ function schedule(options = {}) {
306
+ const result = computeSchedule(docOf(), {
307
+ calendar: calendar.value,
308
+ pull: options.pull
309
+ });
310
+ if (result.moves.size) transact((tr) => {
311
+ for (const [id, patch] of result.moves) tr.updateTask(id, patch);
312
+ }, { schedule: false });
313
+ announce(format(messages.value.scheduled, { count: result.moves.size }));
314
+ return result;
315
+ }
316
+ const selectedLink = ref(null);
317
+ const drag = shallowRef(null);
318
+ const taskList = computed(() => toRaw(tasks.value));
319
+ const linkList = computed(() => toRaw(links.value));
320
+ const shown = computed(() => {
321
+ if (!drag.value?.started) return taskList.value;
322
+ const changes = dragPreview.value;
323
+ if (!changes) return taskList.value;
324
+ return taskList.value.map((task) => {
325
+ const patch = changes.get(task.id);
326
+ return patch ? {
327
+ ...task,
328
+ ...patch
329
+ } : task;
330
+ });
331
+ });
332
+ const tree = computed(() => buildTree(shown.value));
333
+ const rows = computed(() => tree.value.rows);
334
+ const calendar = computed(() => resolveCalendar(props.calendar));
335
+ const selected = computed(() => new Set(selection.value));
336
+ const shownDoc = computed(() => ({
337
+ version: 1,
338
+ tasks: shown.value,
339
+ links: linkList.value
340
+ }));
341
+ /** While dragging with automatic scheduling: the tasks the drop would push, and where to. */
342
+ /** The tasks a drag is about to change, with the ones it would push, as they would be written. */
343
+ const preview = computed(() => {
344
+ const changes = dragPreview.value;
345
+ const pushes = pushPreview.value?.moves;
346
+ if (!changes || !pushes?.size) return changes;
347
+ const out = new Map(changes);
348
+ for (const [id, patch] of pushes) out.set(id, patch);
349
+ return out;
350
+ });
351
+ const pushPreview = computed(() => {
352
+ const changes = dragPreview.value;
353
+ if (props.scheduling !== "auto" || !changes || drag.value?.mode === "progress") return null;
354
+ return computeSchedule(shownDoc.value, {
355
+ calendar: calendar.value,
356
+ fixed: changes.keys()
357
+ });
358
+ });
359
+ /** Links whose later task starts (or ends) before the link allows. */
360
+ const brokenLinks = computed(() => pushPreview.value?.violated ?? findBrokenLinks(shownDoc.value, { calendar: calendar.value }));
361
+ const critical = computed(() => props.critical ? computeCriticalPath(shownDoc.value, { calendar: calendar.value }) : null);
362
+ /** The time the tasks cover, or `null` when nothing is scheduled. */
363
+ const content = computed(() => {
364
+ let start = Infinity;
365
+ let end = -Infinity;
366
+ for (const row of tree.value.byId.values()) {
367
+ if (!row.span) continue;
368
+ start = Math.min(start, row.span.start);
369
+ end = Math.max(end, row.span.end);
370
+ }
371
+ return start > end ? null : {
372
+ start,
373
+ end
374
+ };
375
+ });
376
+ const axis = useTimeAxis({
377
+ chart,
378
+ tableBody,
379
+ viewWidth,
380
+ scale,
381
+ range: () => props.range,
382
+ today: () => props.today,
383
+ weekStart,
384
+ calendar,
385
+ messages,
386
+ content,
387
+ fitWidth: () => showTable.value ? viewWidth.value : rootWidth.value
388
+ });
389
+ const { dayWidth, level, ppm, now, extent, origin, totalWidth, xOf, contentX, scrollTop, scrollLeft, scrollbar, onChartScroll, onTableScroll, scrollToTime, scrollToDate, scrollToToday, fit, zoomTo, zoomIn, zoomOut, onWheel, timeWindow, topCells, bottomCells, shaded, todayX } = axis;
390
+ watch([viewHeight, viewWidth], axis.measureScrollbar, { flush: "post" });
391
+ watch([rootWidth, viewWidth], axis.fitWhenSized);
392
+ const warned = /* @__PURE__ */ new Set();
393
+ watch(tasks, (list) => {
394
+ if (process.env.NODE_ENV === "production") return;
395
+ for (const task of list) for (const key of ["start", "end"]) {
396
+ const text = task[key];
397
+ if (text == null || text === "" || parseTime(text) || warned.has(`${task.id}:${key}`)) continue;
398
+ warned.add(`${task.id}:${key}`);
399
+ console.warn(`[@dou-ui/gantt] task "${task.id}" has a ${key} that can't be read: "${text}". Use YYYY-MM-DD or YYYY-MM-DD HH:mm.`);
400
+ }
401
+ }, { immediate: true });
402
+ const bodyHeight = computed(() => Math.max(0, viewHeight.value - HEADER));
403
+ const rowWindow = computed(() => {
404
+ const h = props.rowHeight;
405
+ return {
406
+ from: Math.max(0, Math.floor(scrollTop.value / h) - OVERSCAN_ROWS),
407
+ to: Math.min(rows.value.length, Math.ceil((scrollTop.value + bodyHeight.value) / h) + OVERSCAN_ROWS)
408
+ };
409
+ });
410
+ const visibleRows = computed(() => rows.value.slice(rowWindow.value.from, rowWindow.value.to));
411
+ /**
412
+ * Without the table, rows whose bar is out of view (or that have none)
413
+ * show their title at the left edge of the timeline.
414
+ */
415
+ const rowTitles = computed(() => {
416
+ if (showTable.value) return [];
417
+ const from = origin.value + scrollLeft.value / ppm.value;
418
+ const to = origin.value + (scrollLeft.value + viewWidth.value) / ppm.value;
419
+ return visibleRows.value.filter((row) => !row.span || row.span.end < from || row.span.start > to).map((row) => ({ row }));
420
+ });
421
+ /**
422
+ * Tasks with a link at their finishing end. Their side titles step further
423
+ * out, past where the line leaves or arrives, so the two don't run together.
424
+ */
425
+ const linkedEnds = computed(() => {
426
+ const out = /* @__PURE__ */ new Set();
427
+ for (const link of linkList.value) {
428
+ const type = link.type ?? "fs";
429
+ if (type === "fs" || type === "ff") out.add(link.from);
430
+ if (type === "ff" || type === "sf") out.add(link.to);
431
+ }
432
+ return out;
433
+ });
434
+ /** Where a bar's side title starts, from the bar's left: clear of a line leaving its end. */
435
+ const sideOffset = (bar) => (bar.row.milestone ? 12 : bar.width + 8) + (linkedEnds.value.has(bar.row.task.id) ? 14 : 0);
436
+ /** Rows titled at the edge; their own side labels would only crowd it. */
437
+ const titled = computed(() => new Set(rowTitles.value.map((item) => item.row.task.id)));
438
+ let canvas;
439
+ /** Width of a bar title in px; estimated where text can't be measured. */
440
+ function textWidth(text) {
441
+ if (canvas === void 0 && root.value) try {
442
+ canvas = document.createElement("canvas").getContext("2d");
443
+ if (canvas) {
444
+ const style = getComputedStyle(root.value);
445
+ canvas.font = `${style.getPropertyValue("--dou-font-size-sm").trim() || "13px"} ${style.fontFamily}`;
446
+ }
447
+ } catch {
448
+ canvas = null;
449
+ }
450
+ return canvas ? canvas.measureText(text).width : text.length * 13;
451
+ }
452
+ function describe(row) {
453
+ const m = messages.value;
454
+ const { task, span } = row;
455
+ if (!span) return format(m.unscheduled, { title: task.title });
456
+ const start = dateLabel(span.start, span.dateOnly, m);
457
+ if (row.milestone) {
458
+ const text = format(m.milestone, {
459
+ title: task.title,
460
+ start
461
+ });
462
+ const change = planChange(row);
463
+ return change ? format(m.baseline.withChange, {
464
+ task: text,
465
+ change
466
+ }) : text;
467
+ }
468
+ const params = {
469
+ title: task.title,
470
+ dates: datesLabel(span, m),
471
+ duration: spokenDuration(span, calendar.value, m),
472
+ progress: Math.round(row.progress * 100)
473
+ };
474
+ let text = format(row.summary ? m.summary : m.task, params);
475
+ if (critical.value?.tasks.has(task.id)) text = format(m.criticalTask, { task: text });
476
+ const change = planChange(row);
477
+ return change ? format(m.baseline.withChange, {
478
+ task: text,
479
+ change
480
+ }) : text;
481
+ }
482
+ const bars = computed(() => {
483
+ const { start, end } = timeWindow.value;
484
+ const h = props.rowHeight;
485
+ const out = [];
486
+ for (const row of visibleRows.value) {
487
+ const span = row.span;
488
+ if (!span || span.end < start - 600 / ppm.value || span.start > end) continue;
489
+ const left = xOf(span.start);
490
+ const width = Math.max(row.milestone ? 0 : 2, (span.end - span.start) * ppm.value);
491
+ let label = props.barLabel === "auto" ? "inside" : props.barLabel;
492
+ if (row.milestone || row.summary) label = props.barLabel === "none" ? "none" : "outside";
493
+ else if (props.barLabel === "auto" && textWidth(row.task.title) + 20 > width) label = "outside";
494
+ out.push({
495
+ row,
496
+ left,
497
+ width,
498
+ top: row.index * h,
499
+ label,
500
+ aria: describe(row)
501
+ });
502
+ }
503
+ return out;
504
+ });
505
+ /** Dashed outlines where a drag would push the waiting tasks to. */
506
+ const ghosts = computed(() => {
507
+ const moves = pushPreview.value?.moves;
508
+ if (!moves?.size) return [];
509
+ const out = [];
510
+ for (const [id, patch] of moves) {
511
+ const row = tree.value.byId.get(id);
512
+ const span = row && row.index >= 0 && spanOf({
513
+ ...row.task,
514
+ ...patch
515
+ });
516
+ if (!span) continue;
517
+ out.push({
518
+ id,
519
+ left: xOf(span.start),
520
+ width: Math.max(2, (span.end - span.start) * ppm.value),
521
+ top: row.index * props.rowHeight,
522
+ milestone: row.milestone
523
+ });
524
+ }
525
+ return out;
526
+ });
527
+ /** How far bars move up to make room for the baseline under them, in px. */
528
+ const planned = computed(() => props.baseline ? baselineSpans(props.baseline, (id) => !!tree.value.byId.get(id)?.milestone) : null);
529
+ const barShift = computed(() => planned.value ? BASELINE_SHIFT : 0);
530
+ /** A thin line under each bar in view, where the baseline had it. */
531
+ const baselineBars = computed(() => {
532
+ const plans = planned.value;
533
+ if (!plans) return [];
534
+ const out = [];
535
+ for (const bar of bars.value) {
536
+ const span = plans.get(bar.row.task.id);
537
+ if (!span) continue;
538
+ out.push({
539
+ id: bar.row.task.id,
540
+ left: xOf(span.start),
541
+ width: Math.max(2, (span.end - span.start) * ppm.value),
542
+ top: bar.top,
543
+ milestone: bar.row.milestone
544
+ });
545
+ }
546
+ return out;
547
+ });
548
+ /** How a task's end compares with its plan, as words; empty when on plan. */
549
+ function planChange(row) {
550
+ const plan = planned.value?.get(row.task.id);
551
+ const span = row.span;
552
+ if (!plan || !span || plan.end === span.end) return "";
553
+ const m = messages.value;
554
+ const gap = Math.abs(span.end - plan.end);
555
+ const duration = plan.dateOnly && span.dateOnly ? format(m.days, { count: Math.round(gap / MINUTES_PER_DAY) }) : format(m.hours, { count: Math.round(gap / 60 * 10) / 10 });
556
+ return format(span.end > plan.end ? m.baseline.late : m.baseline.early, { duration });
557
+ }
558
+ /** Half the width of a milestone's diamond, where lines meet it. */
559
+ function boxOf(row) {
560
+ if (!row.span || row.index < 0) return null;
561
+ const y = row.index * props.rowHeight + props.rowHeight / 2 - barShift.value;
562
+ if (row.milestone) {
563
+ const x = xOf(row.span.start);
564
+ return {
565
+ left: x - MILESTONE,
566
+ right: x + MILESTONE,
567
+ y
568
+ };
569
+ }
570
+ const left = xOf(row.span.start);
571
+ return {
572
+ left,
573
+ right: left + Math.max(2, (row.span.end - row.span.start) * ppm.value),
574
+ y
575
+ };
576
+ }
577
+ const linkPaths = computed(() => {
578
+ if (dayWidth.value < 1) return [];
579
+ const { from, to } = rowWindow.value;
580
+ const xFrom = scrollLeft.value - OVERSCAN_X;
581
+ const xTo = scrollLeft.value + (viewWidth.value || 800) + OVERSCAN_X;
582
+ const out = [];
583
+ for (const link of linkList.value) {
584
+ const a = tree.value.byId.get(link.from);
585
+ const b = tree.value.byId.get(link.to);
586
+ if (!a || !b || a === b) continue;
587
+ if (Math.max(a.index, b.index) < from || Math.min(a.index, b.index) >= to) continue;
588
+ const boxA = boxOf(a);
589
+ const boxB = boxOf(b);
590
+ if (!boxA || !boxB) continue;
591
+ if (Math.max(boxA.right, boxB.right) + 24 < xFrom || Math.min(boxA.left, boxB.left) - 24 > xTo) continue;
592
+ const route = routeLink(boxA, boxB, link.type, props.rowHeight);
593
+ out.push({
594
+ link,
595
+ d: roundedPath(route.points),
596
+ arrow: arrowPath(route.tip, route.direction)
597
+ });
598
+ }
599
+ return out;
600
+ });
601
+ const narrow = computed(() => rootWidth.value > 0 && rootWidth.value < NARROW);
602
+ const showTable = computed(() => props.table && !narrow.value);
603
+ /** The narrowest the table gets, in px. */
604
+ const tableSize = ref(props.tableWidth);
605
+ /** The table leaves the timeline at least 40% of the width. */
606
+ /** The row below the table that adds a task. */
607
+ const showAddRow = computed(() => editable.value && props.addRow && showTable.value);
608
+ /** Rows of height on both sides: the tasks, and the adding row. */
609
+ const bodyRows = computed(() => rows.value.length + (showAddRow.value ? 1 : 0));
610
+ const tableMax = computed(() => Math.max(TABLE_MIN, Math.round((rootWidth.value || 1e3) * .6)));
611
+ const clampTable = (size) => Math.min(tableMax.value, Math.max(TABLE_MIN, size));
612
+ watch(tableMax, () => tableSize.value = clampTable(tableSize.value));
613
+ function onDividerDown(event) {
614
+ if (event.button !== 0) return;
615
+ const el = event.currentTarget;
616
+ el.setPointerCapture(event.pointerId);
617
+ const from = event.clientX;
618
+ const size = tableSize.value;
619
+ const move = (e) => tableSize.value = clampTable(size + e.clientX - from);
620
+ const up = () => {
621
+ el.removeEventListener("pointermove", move);
622
+ el.removeEventListener("pointerup", up);
623
+ el.removeEventListener("pointercancel", up);
624
+ };
625
+ el.addEventListener("pointermove", move);
626
+ el.addEventListener("pointerup", up);
627
+ el.addEventListener("pointercancel", up);
628
+ }
629
+ function onDividerKey(event) {
630
+ const by = event.shiftKey ? 80 : 16;
631
+ if (event.key === "ArrowLeft") tableSize.value = clampTable(tableSize.value - by);
632
+ else if (event.key === "ArrowRight") tableSize.value = clampTable(tableSize.value + by);
633
+ else return;
634
+ event.preventDefault();
635
+ }
636
+ const columns = computed(() => [...props.columns ?? [
637
+ { key: "title" },
638
+ {
639
+ key: "start",
640
+ width: 96
641
+ },
642
+ {
643
+ key: "duration",
644
+ width: 72
645
+ }
646
+ ], ...resolved.value.columns]);
647
+ /** Columns without a width in px are counted at this, or at their `minWidth`. */
648
+ const px = (value) => typeof value === "number" ? value : value?.endsWith("px") ? Number.parseFloat(value) : null;
649
+ /** The narrowest the columns go; past it the table scrolls sideways. */
650
+ const tableMinWidth = computed(() => {
651
+ let total = 0;
652
+ for (const column of columns.value) total += px(column.width) ?? px(column.minWidth) ?? (column.key === "title" ? TITLE_MIN : FLEX_COLUMN);
653
+ return total;
654
+ });
655
+ const tablePane = useTemplateRef("tablePane");
656
+ /** Height of the table's own sideways scrollbar, in px. */
657
+ const tableScrollbar = ref(0);
658
+ watch([
659
+ tableSize,
660
+ tableMinWidth,
661
+ viewHeight,
662
+ showTable
663
+ ], () => {
664
+ const el = tablePane.value;
665
+ tableScrollbar.value = el ? el.offsetHeight - el.clientHeight : 0;
666
+ }, { flush: "post" });
667
+ function headerOf(column) {
668
+ if (column.title != null) return column.title;
669
+ return messages.value.columns[column.key] ?? column.key;
670
+ }
671
+ /** A value of a column of your own, as text. */
672
+ function valueText(column, value, task) {
673
+ if (column.format) return column.format(value, task);
674
+ return value == null ? "" : String(value);
675
+ }
676
+ function cellText(row, column) {
677
+ if (column.value) return valueText(column, column.value(row.task), row.task);
678
+ const m = messages.value;
679
+ const span = row.span;
680
+ switch (column.key) {
681
+ case "title": return row.task.title;
682
+ case "start": return span ? dateLabel(span.start - (row.milestone && span.dateOnly ? MINUTES_PER_DAY / 2 : 0), span.dateOnly, m) : "";
683
+ case "end": return span && !row.milestone ? endLabel(span, m) : "";
684
+ case "duration": return span && !row.milestone ? durationLabel(span, calendar.value, m) : "";
685
+ case "progress": return span && !row.milestone ? `${Math.round(row.progress * 100)}%` : "";
686
+ default: return valueText(column, row.task.data?.[column.key], row.task);
687
+ }
688
+ }
689
+ const cssLength = (value) => typeof value === "number" ? `${value}px` : value;
690
+ function columnStyle(column) {
691
+ const width = cssLength(column.width);
692
+ return {
693
+ flex: width ? `0 0 ${width}` : "1 1 0",
694
+ width,
695
+ minWidth: cssLength(column.minWidth)
696
+ };
697
+ }
698
+ function toggle(row) {
699
+ transact((tr) => tr.updateTask(row.task.id, { collapsed: !row.task.collapsed || void 0 }), { history: false });
700
+ }
701
+ /** The cell being edited; `id` is `null` for the row that adds a task. */
702
+ const editing = ref(null);
703
+ const BUILT_IN_EDITABLE = /* @__PURE__ */ new Set([
704
+ "title",
705
+ "start",
706
+ "end",
707
+ "duration",
708
+ "progress"
709
+ ]);
710
+ /** Whether a cell can be edited: built-in columns unless turned off, others when turned on. */
711
+ function canEdit(row, column) {
712
+ if (!editable.value) return false;
713
+ if (column.editable === false) return false;
714
+ if (!BUILT_IN_EDITABLE.has(column.key) || column.value) return column.editable === true;
715
+ if (column.key === "title") return true;
716
+ if (row.summary || row.task.locked) return false;
717
+ if (row.milestone) return column.key === "start";
718
+ return column.key === "start" || !!row.span;
719
+ }
720
+ /** Starts editing a cell, the task's title by default. */
721
+ function editCell(id, key = "title") {
722
+ const row = tree.value.byId.get(id);
723
+ const column = columns.value.find((item) => item.key === key);
724
+ if (!row || !column || !canEdit(row, column)) return false;
725
+ if (row.index < 0) return false;
726
+ active.value = id;
727
+ scrollToTask(id);
728
+ editing.value = {
729
+ id,
730
+ key
731
+ };
732
+ return true;
733
+ }
734
+ function editorKind(row, column) {
735
+ const key = column.key;
736
+ if (key === "start" || key === "end") return row.span && !row.span.dateOnly ? "datetime" : "date";
737
+ if (key === "duration" || key === "progress") return "number";
738
+ return BUILT_IN_EDITABLE.has(key) ? "text" : column.input ?? "text";
739
+ }
740
+ /** What an editor starts with: the value as it would be typed. */
741
+ function editorValue(row, column) {
742
+ const { task, span } = row;
743
+ const key = column.key;
744
+ switch (key) {
745
+ case "title": return task.title;
746
+ case "start": return task.start ?? "";
747
+ case "end": return span ? formatTime(span.dateOnly ? span.end - 1 : span.end, span.dateOnly) : "";
748
+ case "duration": {
749
+ if (!span) return "";
750
+ const work = span.dateOnly ? workdaysBetween(calendar.value, span.start, span.end) : workingMinutes(calendar.value, span.start, span.end) / 60;
751
+ return String(Math.round(work * 10) / 10);
752
+ }
753
+ case "progress": return String(Math.round(row.progress * 100));
754
+ default: {
755
+ const value = task.data?.[key];
756
+ return value == null ? "" : String(value);
757
+ }
758
+ }
759
+ }
760
+ /**
761
+ * The change an edited value makes to its task: `null` when it makes
762
+ * none, `false` when the value can't be read.
763
+ */
764
+ function editPatch(row, column, text) {
765
+ const { task, span } = row;
766
+ const key = column.key;
767
+ const value = text.trim();
768
+ switch (key) {
769
+ case "title": return value && value !== task.title ? { title: value } : null;
770
+ case "start": {
771
+ const at = parseTime(value);
772
+ if (!at) return false;
773
+ if (row.milestone) return { start: value };
774
+ if (!span) return writeSpan({
775
+ ...task,
776
+ start: value
777
+ }, at.at, at.at + (at.dateOnly ? MINUTES_PER_DAY : 60));
778
+ return writeSpan(task, at.at, endKeepingWork(calendar.value, span, at.at));
779
+ }
780
+ case "end": {
781
+ const at = parseTime(value);
782
+ if (!span) return null;
783
+ const end = at ? at.at + (at.dateOnly ? MINUTES_PER_DAY : 0) : -Infinity;
784
+ return end > span.start ? writeSpan(task, span.start, end) : false;
785
+ }
786
+ case "duration": {
787
+ const count = Number(value);
788
+ if (!span) return null;
789
+ if (!value || !Number.isFinite(count) || count <= 0) return false;
790
+ const end = span.dateOnly ? shiftWorkdays(calendar.value, span.start, count) : shiftWorkMinutes(calendar.value, span.start, Math.round(count * 60));
791
+ return writeSpan(task, span.start, end);
792
+ }
793
+ case "progress": {
794
+ const count = Number(value);
795
+ if (!value || !Number.isFinite(count)) return false;
796
+ return { progress: Math.min(100, Math.max(0, count)) / 100 };
797
+ }
798
+ default: {
799
+ const next = column.parse ? column.parse(text, task) : value;
800
+ if (next === void 0) return false;
801
+ return next === task.data?.[key] ? null : { data: {
802
+ ...task.data,
803
+ [key]: next
804
+ } };
805
+ }
806
+ }
807
+ }
808
+ /** Says a cell's value can't be read. */
809
+ function onEditInvalid(column) {
810
+ announce(format(messages.value.invalidValue, { column: headerOf(column) }));
811
+ }
812
+ function onEditDone(row, column, value) {
813
+ editing.value = null;
814
+ focusTask(row.task.id);
815
+ if (value == null) return;
816
+ const patch = editPatch(row, column, value);
817
+ if (!patch) return;
818
+ if ("start" in patch) {
819
+ const refusal = checkMove(/* @__PURE__ */ new Map([[row.task.id, patch]]));
820
+ if (refusal) return announce(refusal);
821
+ }
822
+ const done = transact((tr) => tr.updateTask(row.task.id, patch));
823
+ announceTask(row.task.id, done);
824
+ }
825
+ /** Tab in the title field indents, Shift+Tab outdents, and editing goes on. */
826
+ function onEditTab(row, back) {
827
+ const id = row.task.id;
828
+ if (back ? outdentTask(id) : indentTask(id)) nextTick(() => editCell(id));
829
+ }
830
+ /** Adds a task at the end, from the row below the table. */
831
+ function onAddDone(value) {
832
+ const title = value?.trim();
833
+ if (value == null) {
834
+ editing.value = null;
835
+ root.value?.focus({ preventScroll: true });
836
+ return;
837
+ }
838
+ if (!title) return;
839
+ const id = createId();
840
+ const last = lastDated();
841
+ const span = newTaskSpan(last && { after: last });
842
+ transact((tr) => tr.addTask({
843
+ id,
844
+ title,
845
+ ...writeSpan({
846
+ id,
847
+ title
848
+ }, span.start, span.end)
849
+ }));
850
+ active.value = id;
851
+ nextTick(() => scrollToTask(id));
852
+ announce(format(messages.value.taskAdded, { title }));
853
+ }
854
+ /** The lowest row that has dates. */
855
+ function lastDated() {
856
+ const list = tree.value.rows;
857
+ for (let i = list.length - 1; i >= 0; i--) if (list[i].span) return list[i];
858
+ }
859
+ /**
860
+ * Dates for a new task, so it shows on the timeline: a working day from
861
+ * `at`, or right after the task above it (a working hour, if that one has
862
+ * times), or from today.
863
+ */
864
+ function newTaskSpan(from) {
865
+ const cal = calendar.value;
866
+ const above = from && "after" in from ? from.after.span : null;
867
+ if (above && !above.dateOnly) {
868
+ const start = nextWorkTime(cal, above.end, false);
869
+ return {
870
+ start,
871
+ end: shiftWorkMinutes(cal, start, 60)
872
+ };
873
+ }
874
+ const day = from && "at" in from ? dayStart(from.at) : above?.end ?? dayStart(now.value);
875
+ const start = nextWorkTime(cal, day, true);
876
+ return {
877
+ start,
878
+ end: shiftWorkdays(cal, start, 1)
879
+ };
880
+ }
881
+ /** Adds a task after another one, or as its last subtask, and starts editing its title. */
882
+ function insertTask(near, options = {}) {
883
+ const { where = "after" } = options;
884
+ const id = createId();
885
+ const title = messages.value.newTask;
886
+ const row = near == null ? void 0 : tree.value.byId.get(near);
887
+ const inside = !!row && where === "inside";
888
+ const at = typeof options.start === "string" ? parseTime(options.start)?.at : options.start;
889
+ const children = row && inside ? tree.value.childrenOf.get(row.task.id) ?? [] : [];
890
+ const above = inside ? children.length ? tree.value.byId.get(children[children.length - 1].id) : void 0 : row ?? lastDated();
891
+ const span = newTaskSpan(at != null ? { at } : above ? { after: above } : row?.span ? { at: row.span.start } : void 0);
892
+ transact((tr) => {
893
+ tr.addTask({
894
+ id,
895
+ title,
896
+ ...writeSpan({
897
+ id,
898
+ title
899
+ }, span.start, span.end)
900
+ });
901
+ if (!row) return;
902
+ const nearId = row.task.id;
903
+ if (inside) {
904
+ tr.moveTask(id, nearId);
905
+ if (row.task.collapsed) tr.updateTask(nearId, { collapsed: void 0 });
906
+ } else {
907
+ const parent = parentOf(nearId);
908
+ const siblings = tree.value.childrenOf.get(parent) ?? [];
909
+ tr.moveTask(id, parent, siblings.findIndex((task) => task.id === nearId) + 1);
910
+ }
911
+ });
912
+ selection.value = [id];
913
+ nextTick(() => editCell(id));
914
+ announce(format(messages.value.taskAdded, { title }));
915
+ return id;
916
+ }
917
+ /** A task's parent as laid out: a missing or looping parent counts as none. */
918
+ function parentOf(id) {
919
+ for (const [parent, children] of tree.value.childrenOf) if (children.some((task) => task.id === id)) return parent;
920
+ }
921
+ /** Makes a task the last subtask of the task above it among its siblings. */
922
+ function indentTask(id) {
923
+ if (!editable.value) return false;
924
+ const siblings = tree.value.childrenOf.get(parentOf(id)) ?? [];
925
+ const index = siblings.findIndex((task) => task.id === id);
926
+ const above = siblings[index - 1];
927
+ if (index < 1 || !above || above.type === "milestone") return false;
928
+ transact((tr) => {
929
+ tr.moveTask(id, above.id);
930
+ if (above.collapsed) tr.updateTask(above.id, { collapsed: void 0 });
931
+ });
932
+ announceMoved(id);
933
+ return true;
934
+ }
935
+ /** Moves a task out of its parent, to just after it. */
936
+ function outdentTask(id) {
937
+ if (!editable.value) return false;
938
+ const parent = parentOf(id);
939
+ if (parent == null) return false;
940
+ const grand = parentOf(parent);
941
+ const index = (tree.value.childrenOf.get(grand) ?? []).findIndex((task) => task.id === parent);
942
+ transact((tr) => tr.moveTask(id, grand, index + 1));
943
+ announceMoved(id);
944
+ return true;
945
+ }
946
+ /** Says where a task went in the outline, once the change has reached the rows. */
947
+ function announceMoved(id) {
948
+ nextTick(() => {
949
+ const byId = tree.value.byId;
950
+ const task = byId.get(id)?.task.title ?? "";
951
+ const up = parentOf(id);
952
+ const parent = up == null ? void 0 : byId.get(up)?.task.title;
953
+ const m = messages.value;
954
+ announce(parent ? format(m.movedUnder, {
955
+ task,
956
+ parent
957
+ }) : format(m.movedTop, { task }));
958
+ });
959
+ }
960
+ /** Removes tasks with their subtasks and links, as one undo step. */
961
+ function removeTasks(ids) {
962
+ if (!editable.value || !ids.length) return;
963
+ const titles = ids.map((id) => tree.value.byId.get(id)?.task.title).filter(Boolean);
964
+ transact((tr) => {
965
+ for (const id of ids) tr.removeTask(id);
966
+ });
967
+ if (active.value && !tasks.value.some((task) => task.id === active.value)) active.value = null;
968
+ announce(format(messages.value.tasksRemoved, { titles: titles.join(messages.value.listSeparator) }));
969
+ }
970
+ function onCellDblclick(row, column, event) {
971
+ if (!canEdit(row, column)) return;
972
+ event.stopPropagation();
973
+ editCell(row.task.id, column.key);
974
+ }
975
+ function startAdding() {
976
+ editing.value = {
977
+ id: null,
978
+ key: "title"
979
+ };
980
+ }
981
+ const rowDrag = shallowRef(null);
982
+ let rowTimer;
983
+ let rowFrame = 0;
984
+ /** The rows moved together: the pressed one and everything under it. */
985
+ const rowDragged = computed(() => {
986
+ const d = rowDrag.value;
987
+ if (!d?.started) return null;
988
+ const out = /* @__PURE__ */ new Set([d.id]);
989
+ const stack = [d.id];
990
+ while (stack.length) for (const child of tree.value.childrenOf.get(stack.pop()) ?? []) {
991
+ out.add(child.id);
992
+ stack.push(child.id);
993
+ }
994
+ return out;
995
+ });
996
+ /** Where a row being dragged would go: before or after a row, or inside it. */
997
+ const rowDrop = computed(() => {
998
+ const d = rowDrag.value;
999
+ const moved = rowDragged.value;
1000
+ if (!d?.started || !moved) return null;
1001
+ const list = rows.value;
1002
+ if (!list.length) return null;
1003
+ const h = props.rowHeight;
1004
+ const index = Math.max(0, Math.min(list.length - 1, Math.floor(d.y / h)));
1005
+ const row = list[index];
1006
+ if (moved.has(row.task.id)) return null;
1007
+ const within = d.y / h - index;
1008
+ let where = within < .3 ? "before" : within > .7 || row.milestone ? "after" : "inside";
1009
+ if (row.milestone && within < .5) where = "before";
1010
+ return {
1011
+ row,
1012
+ where
1013
+ };
1014
+ });
1015
+ /** The line showing where a dragged row goes, indented to the level it would take. */
1016
+ const dropLine = computed(() => {
1017
+ const drop = rowDrop.value;
1018
+ if (!drop || drop.where === "inside") return null;
1019
+ const { row, where } = drop;
1020
+ const first = where === "after" && row.summary && !row.task.collapsed;
1021
+ return {
1022
+ transform: `translateY(${(row.index + (where === "after" ? 1 : 0)) * props.rowHeight}px)`,
1023
+ left: `${(row.depth + (first ? 1 : 0)) * 16 + 30}px`
1024
+ };
1025
+ });
1026
+ function onRowDown(event, row) {
1027
+ if (event.button !== 0 || !editable.value || editing.value) return;
1028
+ if (event.target.closest("button, input, .dou-gantt__editor")) return;
1029
+ const body = tableBody.value;
1030
+ const y = event.clientY - body.getBoundingClientRect().top + body.scrollTop;
1031
+ const d = {
1032
+ id: row.task.id,
1033
+ pointerId: event.pointerId,
1034
+ touch: event.pointerType === "touch",
1035
+ clientX: event.clientX,
1036
+ y0: y,
1037
+ y,
1038
+ clientY: event.clientY,
1039
+ started: false
1040
+ };
1041
+ rowDrag.value = d;
1042
+ hold?.release();
1043
+ hold = holdPointer(event, root.value);
1044
+ window.addEventListener("pointermove", onRowMove);
1045
+ window.addEventListener("pointerup", onRowUp);
1046
+ window.addEventListener("pointercancel", stopRowDrag);
1047
+ window.addEventListener("keydown", onRowKey, true);
1048
+ if (d.touch) rowTimer = setTimeout(() => {
1049
+ if (rowDrag.value !== d) return;
1050
+ beginRowDrag(d);
1051
+ navigator.vibrate?.(8);
1052
+ }, LONG_PRESS);
1053
+ }
1054
+ function beginRowDrag(d) {
1055
+ d.started = true;
1056
+ hold?.grab();
1057
+ window.getSelection()?.removeAllRanges();
1058
+ rowFrame = requestAnimationFrame(rowAutoScroll);
1059
+ rowDrag.value = { ...d };
1060
+ }
1061
+ function onRowMove(event) {
1062
+ const d = rowDrag.value;
1063
+ if (!d || event.pointerId !== d.pointerId) return;
1064
+ const body = tableBody.value;
1065
+ const y = event.clientY - body.getBoundingClientRect().top + body.scrollTop;
1066
+ if (!d.started) {
1067
+ if (Math.abs(y - d.y0) <= THRESHOLD[d.touch ? "touch" : "mouse"]) return;
1068
+ if (d.touch) return stopRowDrag();
1069
+ beginRowDrag(d);
1070
+ }
1071
+ rowDrag.value = {
1072
+ ...rowDrag.value,
1073
+ y,
1074
+ clientY: event.clientY
1075
+ };
1076
+ }
1077
+ function rowAutoScroll() {
1078
+ const d = rowDrag.value;
1079
+ const body = tableBody.value;
1080
+ if (!d?.started || !body) return;
1081
+ const rect = body.getBoundingClientRect();
1082
+ let dy = -edgeSpeed(d.clientY - rect.top) || edgeSpeed(rect.bottom - d.clientY);
1083
+ if (Math.sign(dy) !== Math.sign(d.y - d.y0)) dy = 0;
1084
+ if (dy) {
1085
+ body.scrollTop += dy;
1086
+ onTableScroll();
1087
+ rowDrag.value = {
1088
+ ...d,
1089
+ y: d.clientY - rect.top + body.scrollTop
1090
+ };
1091
+ }
1092
+ rowFrame = requestAnimationFrame(rowAutoScroll);
1093
+ }
1094
+ function onRowUp(event) {
1095
+ const d = rowDrag.value;
1096
+ if (!d || event.pointerId !== d.pointerId) return;
1097
+ const drop = rowDrop.value;
1098
+ if (d.touch && d.started && Math.abs(d.y - d.y0) < THRESHOLD.touch) {
1099
+ stopRowDrag();
1100
+ if (!menuAllowed(task(d.id), d.clientX, event.clientY)) return;
1101
+ showMenu({
1102
+ kind: "task",
1103
+ task: task(d.id)
1104
+ }, d.clientX, event.clientY);
1105
+ return;
1106
+ }
1107
+ if (d.started) {
1108
+ dragged = true;
1109
+ setTimeout(() => dragged = false);
1110
+ if (drop) dropRow(d.id, drop.row, drop.where);
1111
+ }
1112
+ stopRowDrag();
1113
+ }
1114
+ function dropRow(id, target, where) {
1115
+ const targetId = target.task.id;
1116
+ transact((tr) => {
1117
+ if (where === "inside") {
1118
+ tr.moveTask(id, targetId);
1119
+ if (target.task.collapsed) tr.updateTask(targetId, { collapsed: void 0 });
1120
+ return;
1121
+ }
1122
+ if (where === "after" && target.summary && !target.task.collapsed) return void tr.moveTask(id, targetId, 0);
1123
+ const parent = parentOf(targetId);
1124
+ const index = (tree.value.childrenOf.get(parent) ?? []).filter((task) => task.id !== id).findIndex((task) => task.id === targetId);
1125
+ tr.moveTask(id, parent, where === "before" ? index : index + 1);
1126
+ });
1127
+ announceTask(id);
1128
+ }
1129
+ function onRowKey(event) {
1130
+ if (event.key !== "Escape" || !rowDrag.value) return;
1131
+ event.preventDefault();
1132
+ event.stopPropagation();
1133
+ stopRowDrag();
1134
+ }
1135
+ function stopRowDrag() {
1136
+ clearTimeout(rowTimer);
1137
+ rowTimer = void 0;
1138
+ cancelAnimationFrame(rowFrame);
1139
+ hold?.release();
1140
+ hold = null;
1141
+ window.removeEventListener("pointermove", onRowMove);
1142
+ window.removeEventListener("pointerup", onRowUp);
1143
+ window.removeEventListener("pointercancel", stopRowDrag);
1144
+ window.removeEventListener("keydown", onRowKey, true);
1145
+ rowDrag.value = null;
1146
+ }
1147
+ onBeforeUnmount(stopRowDrag);
1148
+ const hovered = ref(null);
1149
+ function select(task, event) {
1150
+ const id = task.id;
1151
+ if (event.shiftKey || event.metaKey || event.ctrlKey) selection.value = selected.value.has(id) ? selection.value.filter((item) => item !== id) : [...selection.value, id];
1152
+ else if (selection.value.length !== 1 || selection.value[0] !== id) selection.value = [id];
1153
+ }
1154
+ function onTaskClick(row, event) {
1155
+ if (dragged) return;
1156
+ selectedLink.value = null;
1157
+ active.value = row.task.id;
1158
+ select(row.task, event);
1159
+ emit("taskClick", row.task, event);
1160
+ }
1161
+ const highlightRows = computed(() => visibleRows.value.filter((row) => selected.value.has(row.task.id) || hovered.value === row.task.id));
1162
+ const THRESHOLD = {
1163
+ mouse: 3,
1164
+ touch: 6
1165
+ };
1166
+ /** How long a finger rests on a bar before it picks it up, in ms. */
1167
+ let dragged = false;
1168
+ let pressTimer;
1169
+ let frame = 0;
1170
+ /** The step a drag snaps to for these spans: whole days when any is date-only. */
1171
+ function stepFor(spans) {
1172
+ const step = snapStep(dayWidth.value);
1173
+ for (const span of spans) if (span.dateOnly) return Math.max(step, MINUTES_PER_DAY);
1174
+ return step;
1175
+ }
1176
+ const snap = (at, step) => Math.round(at / step) * step;
1177
+ const task = (id) => tasks.value.find((item) => item.id === id);
1178
+ /** Auto-scroll speed in px per frame, `gap` px from the edge. */
1179
+ const edgeSpeed = (gap) => gap < EDGE ? Math.ceil((EDGE - gap) / EDGE * 16) : 0;
1180
+ /** The changes the current drag would make, by task id. */
1181
+ const dragPreview = computed(() => {
1182
+ const d = drag.value;
1183
+ if (!d?.started) return null;
1184
+ const out = /* @__PURE__ */ new Map();
1185
+ const delta = (d.x - d.x0) / ppm.value;
1186
+ switch (d.mode) {
1187
+ case "move": {
1188
+ const unit = stepFor(d.spans.values());
1189
+ const first = d.spans.get(d.id) ?? d.spans.values().next().value;
1190
+ const shift = snap(first.start + delta, unit) - first.start;
1191
+ for (const [id, span] of d.spans) {
1192
+ const start = span.start + shift;
1193
+ out.set(id, writeSpan(task(id), start, endKeepingWork(calendar.value, span, start)));
1194
+ }
1195
+ break;
1196
+ }
1197
+ case "start":
1198
+ case "end": {
1199
+ const span = d.spans.get(d.id);
1200
+ const unit = stepFor([span]);
1201
+ if (d.mode === "start") {
1202
+ const start = Math.min(snap(span.start + delta, unit), span.end - unit);
1203
+ out.set(d.id, writeSpan(task(d.id), start, span.end));
1204
+ } else {
1205
+ const end = Math.max(snap(span.end + delta, unit), span.start + unit);
1206
+ out.set(d.id, writeSpan(task(d.id), span.start, end));
1207
+ }
1208
+ break;
1209
+ }
1210
+ case "progress": {
1211
+ const { left, width } = d.box;
1212
+ const value = Math.round(Math.min(1, Math.max(0, (d.x - left) / width)) * 20) / 20;
1213
+ out.set(d.id, { progress: value });
1214
+ break;
1215
+ }
1216
+ case "draw": {
1217
+ const unit = Math.max(snapStep(dayWidth.value), MINUTES_PER_DAY);
1218
+ const a = origin.value + Math.min(d.x0, d.x) / ppm.value;
1219
+ const b = origin.value + Math.max(d.x0, d.x) / ppm.value;
1220
+ const start = Math.floor(a / unit) * unit;
1221
+ const end = Math.max(start + unit, Math.ceil(b / unit) * unit);
1222
+ out.set(d.id, writeSpan(task(d.id), start, end));
1223
+ break;
1224
+ }
1225
+ case "link": return null;
1226
+ }
1227
+ return out;
1228
+ });
1229
+ /** The leaf tasks a drag on `row` moves: the row itself, its subtasks, or the whole selection. */
1230
+ function movers(row) {
1231
+ const out = /* @__PURE__ */ new Map();
1232
+ const add = (id) => {
1233
+ const item = tree.value.byId.get(id);
1234
+ if (!item || item.task.locked) return;
1235
+ if (item.summary) for (const child of tree.value.childrenOf.get(id) ?? []) add(child.id);
1236
+ else if (item.span) out.set(id, { ...item.span });
1237
+ };
1238
+ const ids = selected.value.has(row.task.id) ? selection.value : [row.task.id];
1239
+ for (const id of ids) add(id);
1240
+ return out;
1241
+ }
1242
+ function startDrag(event, row, mode, from) {
1243
+ if (event.button !== 0 || !editable.value) return;
1244
+ const touch = event.pointerType === "touch";
1245
+ hold?.release();
1246
+ hold = holdPointer(event, root.value);
1247
+ const x = contentX(event.clientX);
1248
+ const d = {
1249
+ mode,
1250
+ id: row.task.id,
1251
+ pointerId: event.pointerId,
1252
+ touch,
1253
+ x0: x,
1254
+ x,
1255
+ clientX: event.clientX,
1256
+ clientY: event.clientY,
1257
+ started: false,
1258
+ spans: /* @__PURE__ */ new Map(),
1259
+ from
1260
+ };
1261
+ if (mode === "move") {
1262
+ d.spans = movers(row);
1263
+ if (!d.spans.size) return;
1264
+ } else if (mode === "start" || mode === "end") {
1265
+ if (!row.span || row.task.locked) return;
1266
+ d.spans.set(row.task.id, { ...row.span });
1267
+ } else if (mode === "progress") {
1268
+ if (!row.span || row.task.locked) return;
1269
+ d.box = {
1270
+ left: xOf(row.span.start),
1271
+ width: Math.max(1, (row.span.end - row.span.start) * ppm.value)
1272
+ };
1273
+ }
1274
+ drag.value = d;
1275
+ window.addEventListener("pointermove", onDragMove);
1276
+ window.addEventListener("pointerup", onDragEnd);
1277
+ window.addEventListener("pointercancel", onDragCancel);
1278
+ window.addEventListener("keydown", onDragKey, true);
1279
+ if (touch && mode === "move") pressTimer = setTimeout(() => {
1280
+ if (drag.value !== d) return;
1281
+ begin(d);
1282
+ navigator.vibrate?.(8);
1283
+ }, LONG_PRESS);
1284
+ }
1285
+ function begin(d) {
1286
+ d.started = true;
1287
+ hold?.grab();
1288
+ frame = requestAnimationFrame(autoScroll);
1289
+ drag.value = { ...d };
1290
+ }
1291
+ let hold = null;
1292
+ const onTouchMove = (event) => void axis.pinchMove(event);
1293
+ const onTouchEnd = (event) => axis.pinchEnd(event);
1294
+ function onTouchStart(event) {
1295
+ if (!axis.pinchStart(event)) return;
1296
+ stopDrag();
1297
+ stopRowDrag();
1298
+ }
1299
+ onMounted(() => {
1300
+ const el = root.value;
1301
+ el.addEventListener("touchstart", onTouchStart, { passive: true });
1302
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
1303
+ el.addEventListener("touchend", onTouchEnd);
1304
+ el.addEventListener("touchcancel", onTouchEnd);
1305
+ });
1306
+ onBeforeUnmount(() => {
1307
+ const el = root.value;
1308
+ el?.removeEventListener("touchstart", onTouchStart);
1309
+ el?.removeEventListener("touchmove", onTouchMove);
1310
+ el?.removeEventListener("touchend", onTouchEnd);
1311
+ el?.removeEventListener("touchcancel", onTouchEnd);
1312
+ });
1313
+ function onDragMove(event) {
1314
+ const d = drag.value;
1315
+ if (!d || event.pointerId !== d.pointerId) return;
1316
+ d.clientX = event.clientX;
1317
+ d.clientY = event.clientY;
1318
+ const x = contentX(event.clientX);
1319
+ if (!d.started) {
1320
+ if (!(Math.abs(x - d.x0) > THRESHOLD[d.touch ? "touch" : "mouse"])) return;
1321
+ if (d.touch && d.mode === "move") return stopDrag();
1322
+ begin(d);
1323
+ }
1324
+ update(drag.value, x);
1325
+ }
1326
+ function update(d, x) {
1327
+ const next = {
1328
+ ...d,
1329
+ x
1330
+ };
1331
+ if (d.mode === "link") next.target = linkTarget(d);
1332
+ drag.value = next;
1333
+ }
1334
+ let canLinkWarned = false;
1335
+ function warnCanLink() {
1336
+ if (process.env.NODE_ENV === "production" || canLinkWarned) return;
1337
+ canLinkWarned = true;
1338
+ console.warn("[@dou-ui/gantt] canLink returned undefined, so the link is refused. Return true to allow it.");
1339
+ }
1340
+ /** The bar end under the pointer, and whether a link to it would be refused. */
1341
+ function linkTarget(d) {
1342
+ const el = document.elementsFromPoint(d.clientX, d.clientY).find((item) => item instanceof HTMLElement && !!item.dataset.task);
1343
+ const id = el?.dataset.task;
1344
+ if (!el || !id) return void 0;
1345
+ const rect = el.getBoundingClientRect();
1346
+ const end = d.clientX < rect.left + rect.width / 2 ? "start" : "end";
1347
+ return {
1348
+ id,
1349
+ end,
1350
+ ...checkLink(d.id, id, linkTypeOf(d.from, end))
1351
+ };
1352
+ }
1353
+ /** Whether a link may be made, and if not, why: the built-in rules, then `can-link` and the extensions'. */
1354
+ function checkLink(from, to, type) {
1355
+ let refusal = linkRefusal(new Map(tasks.value.map((t) => [t.id, t])), links.value, from, to);
1356
+ let reason;
1357
+ if (refusal && refusal !== "self" && refusal !== "custom") reason = messages.value.linkRefusals[refusal];
1358
+ if (refusal) return {
1359
+ refusal,
1360
+ reason
1361
+ };
1362
+ const link = {
1363
+ from,
1364
+ to,
1365
+ type
1366
+ };
1367
+ for (const rule of [...props.canLink ? [props.canLink] : [], ...resolved.value.linkRules]) {
1368
+ const answer = rule(link, docOf());
1369
+ if (answer === true) continue;
1370
+ if (answer === void 0) warnCanLink();
1371
+ return {
1372
+ refusal: "custom",
1373
+ reason: typeof answer === "string" && answer ? answer : void 0
1374
+ };
1375
+ }
1376
+ return {
1377
+ refusal: null,
1378
+ reason
1379
+ };
1380
+ }
1381
+ /**
1382
+ * Why new dates can't be written, from `can-move` and the extensions'; a
1383
+ * refusal without words gets a plain one. `null` when they can.
1384
+ */
1385
+ function checkMove(changes) {
1386
+ const rules = [...props.canMove ? [props.canMove] : [], ...resolved.value.moveRules];
1387
+ if (!rules.length) return null;
1388
+ const doc = docOf();
1389
+ for (const [id, patch] of changes) {
1390
+ if (patch.start === void 0) continue;
1391
+ const move = {
1392
+ task: task(id),
1393
+ start: patch.start,
1394
+ end: patch.end
1395
+ };
1396
+ for (const rule of rules) {
1397
+ const answer = rule(move, doc);
1398
+ if (answer === true) continue;
1399
+ return typeof answer === "string" && answer ? answer : messages.value.moveRefused;
1400
+ }
1401
+ }
1402
+ return null;
1403
+ }
1404
+ /** Why the current drag's dates would be refused, its own or those of the tasks it pushes. */
1405
+ const dragRefusal = computed(() => {
1406
+ const changes = dragPreview.value;
1407
+ if (!changes || drag.value?.mode === "progress") return null;
1408
+ const pushes = pushPreview.value?.moves;
1409
+ return checkMove(pushes?.size ? new Map([...changes, ...pushes]) : changes);
1410
+ });
1411
+ function autoScroll() {
1412
+ const d = drag.value;
1413
+ const el = chart.value;
1414
+ if (!d?.started || !el) return;
1415
+ const rect = el.getBoundingClientRect();
1416
+ let dx = -edgeSpeed(d.clientX - rect.left) || edgeSpeed(rect.right - d.clientX);
1417
+ if (Math.sign(dx) !== Math.sign(d.x - d.x0)) dx = 0;
1418
+ const dy = -edgeSpeed(d.clientY - rect.top - HEADER) || edgeSpeed(rect.bottom - d.clientY);
1419
+ if (dx || dy && d.mode === "link") {
1420
+ el.scrollLeft += dx;
1421
+ if (d.mode === "link") el.scrollTop += dy;
1422
+ update(d, contentX(d.clientX));
1423
+ }
1424
+ frame = requestAnimationFrame(autoScroll);
1425
+ }
1426
+ function onDragEnd(event) {
1427
+ const d = drag.value;
1428
+ if (!d || event.pointerId !== d.pointerId) return;
1429
+ if (d.touch && d.started && d.mode === "move" && Math.abs(d.x - d.x0) < THRESHOLD.touch) {
1430
+ stopDrag();
1431
+ if (!menuAllowed(task(d.id), event.clientX, event.clientY)) return;
1432
+ showMenu({
1433
+ kind: "task",
1434
+ task: task(d.id)
1435
+ }, event.clientX, event.clientY);
1436
+ return;
1437
+ }
1438
+ if (d.started) {
1439
+ dragged = true;
1440
+ setTimeout(() => dragged = false);
1441
+ if (d.mode === "link") finishLink(d);
1442
+ else if (dragRefusal.value) announce(dragRefusal.value);
1443
+ else {
1444
+ const changes = dragPreview.value;
1445
+ if (changes) {
1446
+ const done = transact((tr) => {
1447
+ for (const [id, patch] of changes) tr.updateTask(id, patch);
1448
+ });
1449
+ announceTask(d.id, done);
1450
+ }
1451
+ }
1452
+ }
1453
+ stopDrag();
1454
+ }
1455
+ function finishLink(d) {
1456
+ const target = d.target;
1457
+ if (!target || target.refusal) return;
1458
+ const link = {
1459
+ id: createId(),
1460
+ from: d.id,
1461
+ to: target.id
1462
+ };
1463
+ const type = linkTypeOf(d.from, target.end);
1464
+ if (type !== "fs") link.type = type;
1465
+ const done = transact((tr) => tr.addLink(link));
1466
+ emit("link", link);
1467
+ announceLinked(link, done);
1468
+ }
1469
+ function announceLinked(link, tr) {
1470
+ const m = messages.value;
1471
+ announce(withPushed(format(m.linked, {
1472
+ from: task(link.from).title,
1473
+ to: task(link.to).title
1474
+ }), tr));
1475
+ }
1476
+ /** Adds how many tasks automatic scheduling pushed, if any. */
1477
+ function withPushed(text, tr) {
1478
+ const count = pushedBy.get(tr);
1479
+ return count ? format(messages.value.pushed, {
1480
+ task: text,
1481
+ count
1482
+ }) : text;
1483
+ }
1484
+ const onDragCancel = () => stopDrag();
1485
+ function onDragKey(event) {
1486
+ if (event.key !== "Escape" || !drag.value) return;
1487
+ event.preventDefault();
1488
+ event.stopPropagation();
1489
+ if (drag.value.started) {
1490
+ dragged = true;
1491
+ setTimeout(() => dragged = false);
1492
+ }
1493
+ stopDrag();
1494
+ }
1495
+ function stopDrag() {
1496
+ clearTimeout(pressTimer);
1497
+ cancelAnimationFrame(frame);
1498
+ hold?.release();
1499
+ hold = null;
1500
+ window.removeEventListener("pointermove", onDragMove);
1501
+ window.removeEventListener("pointerup", onDragEnd);
1502
+ window.removeEventListener("pointercancel", onDragCancel);
1503
+ window.removeEventListener("keydown", onDragKey, true);
1504
+ drag.value = null;
1505
+ }
1506
+ onBeforeUnmount(stopDrag);
1507
+ function onBarDown(event, row) {
1508
+ const handle = event.target.closest("[data-handle]")?.dataset.handle;
1509
+ if (handle?.startsWith("link-")) {
1510
+ event.stopPropagation();
1511
+ return startDrag(event, row, "link", handle.slice(5));
1512
+ }
1513
+ if (handle) event.stopPropagation();
1514
+ startDrag(event, row, handle ?? "move");
1515
+ }
1516
+ /** A press on an empty row of a task without dates draws its dates. */
1517
+ function onBodyDown(event) {
1518
+ if (event.target !== event.currentTarget && !event.target.matches(".dou-gantt__off, .dou-gantt__gridline, .dou-gantt__row-highlight")) return;
1519
+ const y = event.clientY - chart.value.getBoundingClientRect().top - HEADER + chart.value.scrollTop;
1520
+ const row = rows.value[Math.floor(y / props.rowHeight)];
1521
+ if (!row || row.span || row.summary || row.task.locked) return;
1522
+ if (event.pointerType === "touch") return;
1523
+ startDrag(event, row, "draw");
1524
+ }
1525
+ /** The bar a link being drawn would end at, and whether it is refused. */
1526
+ const linkHighlight = computed(() => {
1527
+ const target = drag.value?.target;
1528
+ if (target) return {
1529
+ id: target.id,
1530
+ refused: !!target.refusal
1531
+ };
1532
+ const link = keyLink.value;
1533
+ return link?.to ? {
1534
+ id: link.to,
1535
+ refused: !!link.refusal
1536
+ } : null;
1537
+ });
1538
+ /** The line drawn while linking: from the end it started at to the pointer or target. */
1539
+ const draftLink = computed(() => {
1540
+ const link = keyLink.value;
1541
+ if (link?.to) {
1542
+ const a = tree.value.byId.get(link.from);
1543
+ const b = tree.value.byId.get(link.to);
1544
+ const boxA = a && boxOf(a);
1545
+ const boxB = b && boxOf(b);
1546
+ if (!boxA || !boxB) return null;
1547
+ return {
1548
+ d: roundedPath(routeLink(boxA, boxB, "fs", props.rowHeight).points),
1549
+ refused: !!link.refusal
1550
+ };
1551
+ }
1552
+ const d = drag.value;
1553
+ if (d?.mode !== "link" || !d.started) return null;
1554
+ const row = tree.value.byId.get(d.id);
1555
+ const box = row && boxOf(row);
1556
+ if (!box) return null;
1557
+ const x = d.from === "start" ? box.left : box.right;
1558
+ const rect = chart.value.getBoundingClientRect();
1559
+ const y = d.clientY - rect.top - HEADER + chart.value.scrollTop;
1560
+ return {
1561
+ d: `M${x} ${box.y} L${d.x} ${y}`,
1562
+ refused: !!d.target?.refusal
1563
+ };
1564
+ });
1565
+ /** Keeps a tip inside the view: one that starts left of it, or runs past its right edge. */
1566
+ function tipLeft(x, text) {
1567
+ const right = scrollLeft.value + viewWidth.value - textWidth(text) - 24;
1568
+ return Math.max(Math.min(x, right), scrollLeft.value + 8);
1569
+ }
1570
+ /** What a drag shows beside the pointer: new dates, the progress, or why it can't. */
1571
+ const dragTip = computed(() => {
1572
+ const d = drag.value;
1573
+ if (!d?.started) return null;
1574
+ if (d.mode === "link") {
1575
+ const target = d.target?.reason ? tree.value.byId.get(d.target.id) : void 0;
1576
+ if (!target?.span || target.index < 0) return null;
1577
+ return {
1578
+ text: d.target.reason,
1579
+ refused: true,
1580
+ left: tipLeft(xOf(target.span.start), d.target.reason),
1581
+ top: target.index * props.rowHeight
1582
+ };
1583
+ }
1584
+ const row = tree.value.byId.get(d.id);
1585
+ if (!row?.span || row.index < 0) return null;
1586
+ const m = messages.value;
1587
+ const text = dragRefusal.value ? dragRefusal.value : d.mode === "progress" ? format(m.progressTip, { value: Math.round(row.progress * 100) }) : row.milestone ? dateLabel(row.span.start - (row.span.dateOnly ? MINUTES_PER_DAY / 2 : 0), row.span.dateOnly, m) : format(m.rangeTip, {
1588
+ start: dateLabel(row.span.start, row.span.dateOnly, m),
1589
+ end: endLabel(row.span, m),
1590
+ duration: durationLabel(row.span, calendar.value, m)
1591
+ });
1592
+ return {
1593
+ text,
1594
+ refused: !!dragRefusal.value,
1595
+ left: tipLeft(xOf(row.span.start), text),
1596
+ top: row.index * props.rowHeight
1597
+ };
1598
+ });
1599
+ const menu = useTemplateRef("menu");
1600
+ const menuItems = shallowRef([]);
1601
+ const menuOpen = ref(false);
1602
+ /** The built-in items for a target. */
1603
+ function builtInItems(target) {
1604
+ const m = messages.value.menu;
1605
+ if (target.kind === "link") return [{
1606
+ key: "remove-link",
1607
+ label: m.removeLink,
1608
+ icon: TrashIcon,
1609
+ tone: "danger",
1610
+ run: () => {
1611
+ transact((tr) => tr.removeLink(target.link.id));
1612
+ announce(messages.value.linkRemoved);
1613
+ }
1614
+ }];
1615
+ if (target.kind === "canvas") return [{
1616
+ key: "add",
1617
+ label: m.addTask,
1618
+ icon: PlusIcon,
1619
+ run: () => insertTask(null, { start: target.time })
1620
+ }];
1621
+ const id = target.task.id;
1622
+ const row = tree.value.byId.get(id);
1623
+ const siblings = tree.value.childrenOf.get(parentOf(id)) ?? [];
1624
+ const index = siblings.findIndex((item) => item.id === id);
1625
+ const ids = selected.value.has(id) ? selection.value : [id];
1626
+ return [
1627
+ {
1628
+ key: "rename",
1629
+ label: m.rename,
1630
+ icon: EditIcon,
1631
+ shortcut: "f2",
1632
+ run: () => editCell(id)
1633
+ },
1634
+ {
1635
+ key: "add-after",
1636
+ label: m.addAfter,
1637
+ icon: PlusIcon,
1638
+ run: () => insertTask(id)
1639
+ },
1640
+ {
1641
+ key: "add-inside",
1642
+ label: m.addInside,
1643
+ icon: ListTreeIcon,
1644
+ disabled: !!row?.milestone,
1645
+ run: () => insertTask(id, { where: "inside" })
1646
+ },
1647
+ {
1648
+ key: "indent",
1649
+ label: m.indent,
1650
+ icon: IndentIcon,
1651
+ shortcut: "mod+]",
1652
+ divided: true,
1653
+ disabled: index < 1 || siblings[index - 1]?.type === "milestone",
1654
+ run: () => indentTask(id)
1655
+ },
1656
+ {
1657
+ key: "outdent",
1658
+ label: m.outdent,
1659
+ icon: OutdentIcon,
1660
+ shortcut: "mod+[",
1661
+ disabled: parentOf(id) == null,
1662
+ run: () => outdentTask(id)
1663
+ },
1664
+ {
1665
+ key: "remove",
1666
+ label: ids.length > 1 ? format(m.removeTasks, { count: ids.length }) : m.removeTask,
1667
+ icon: TrashIcon,
1668
+ shortcut: "delete",
1669
+ tone: "danger",
1670
+ divided: true,
1671
+ run: () => removeTasks(ids)
1672
+ }
1673
+ ];
1674
+ }
1675
+ /** Your items, the extensions', then the built-in ones, a separator between each group. */
1676
+ function itemsFor(target) {
1677
+ const items = [];
1678
+ const groups = [props.contextMenu?.(target) ?? [], ...resolved.value.menus.map((fn) => fn(context, target))];
1679
+ if (props.menu && editable.value) groups.push(builtInItems(target));
1680
+ for (const list of groups) list.forEach((item, i) => items.push(i === 0 && items.length ? {
1681
+ ...item,
1682
+ divided: true
1683
+ } : item));
1684
+ return items;
1685
+ }
1686
+ function menuPoint(on, x, y) {
1687
+ const rect = chart.value.getBoundingClientRect();
1688
+ const point = {
1689
+ x: x - rect.left + chart.value.scrollLeft,
1690
+ y: y - rect.top - HEADER + chart.value.scrollTop
1691
+ };
1692
+ if (on.kind === "task" && (x < rect.left || x > rect.right)) {
1693
+ const row = tree.value.byId.get(on.task.id);
1694
+ if (row?.span) point.x = xOf(row.span.start);
1695
+ if (row && row.index >= 0) point.y = (row.index + .5) * props.rowHeight;
1696
+ }
1697
+ return {
1698
+ point,
1699
+ time: Math.round(origin.value + point.x / ppm.value)
1700
+ };
1701
+ }
1702
+ /** Opens the menu for a target at a point on screen; `false` when it has nothing to show. */
1703
+ function showMenu(on, x, y) {
1704
+ const target = {
1705
+ ...on,
1706
+ ...menuPoint(on, x, y)
1707
+ };
1708
+ const items = itemsFor(target);
1709
+ if (!items.length) return false;
1710
+ if (target.kind === "task") {
1711
+ active.value = target.task.id;
1712
+ if (!selected.value.has(target.task.id)) selection.value = [target.task.id];
1713
+ } else if (target.kind === "link") selectedLink.value = target.link.id;
1714
+ menuItems.value = items;
1715
+ menu.value?.open(x, y);
1716
+ return true;
1717
+ }
1718
+ /** When a finger last went down: its long press opens the menu itself, not the browser's. */
1719
+ let touchedAt = 0;
1720
+ const onRootPointerdown = (event) => {
1721
+ if (event.pointerType === "touch") touchedAt = Date.now();
1722
+ };
1723
+ /**
1724
+ * A menu opened by a resting finger or by the menu key has no browser event:
1725
+ * it gets a stand-in, so `taskContextmenu` comes first in every case and
1726
+ * `preventDefault()` keeps the menu closed.
1727
+ */
1728
+ function menuAllowed(item, x, y) {
1729
+ const event = new MouseEvent("contextmenu", {
1730
+ cancelable: true,
1731
+ clientX: x,
1732
+ clientY: y
1733
+ });
1734
+ emit("taskContextmenu", item, event);
1735
+ return !event.defaultPrevented;
1736
+ }
1737
+ function onTaskContextmenu(row, event) {
1738
+ if (Date.now() - touchedAt < 1e3) return event.preventDefault();
1739
+ emit("taskContextmenu", row.task, event);
1740
+ if (event.defaultPrevented) return;
1741
+ if (showMenu({
1742
+ kind: "task",
1743
+ task: row.task
1744
+ }, event.clientX, event.clientY)) event.preventDefault();
1745
+ }
1746
+ function onLinkContextmenu(link, event) {
1747
+ emit("linkContextmenu", link, event);
1748
+ if (event.defaultPrevented) return;
1749
+ if (showMenu({
1750
+ kind: "link",
1751
+ link
1752
+ }, event.clientX, event.clientY)) event.preventDefault();
1753
+ }
1754
+ function onBodyContextmenu(event) {
1755
+ if (event.defaultPrevented || event.target !== event.currentTarget) {
1756
+ if (!event.target.matches(".dou-gantt__off, .dou-gantt__gridline")) return;
1757
+ }
1758
+ if (Date.now() - touchedAt < 1e3) return event.preventDefault();
1759
+ if (showMenu({ kind: "canvas" }, event.clientX, event.clientY)) event.preventDefault();
1760
+ }
1761
+ /** The empty timeline below the last row opens the same menu as the rows' blank space. */
1762
+ function onCanvasContextmenu(event) {
1763
+ if (event.target !== event.currentTarget || event.defaultPrevented) return;
1764
+ if (Date.now() - touchedAt < 1e3) return event.preventDefault();
1765
+ if (showMenu({ kind: "canvas" }, event.clientX, event.clientY)) event.preventDefault();
1766
+ }
1767
+ /** The menu key, or Shift+F10, opens the menu of the task the keyboard is on, at its bar. */
1768
+ function openKeyMenu() {
1769
+ const id = active.value;
1770
+ const row = id ? tree.value.byId.get(id) : void 0;
1771
+ if (!row) return false;
1772
+ const rect = (root.value?.querySelector(`.dou-gantt__bar[data-task="${CSS.escape(row.task.id)}"]`) ?? root.value?.querySelector(`.dou-gantt__tr[data-row="${CSS.escape(row.task.id)}"]`))?.getBoundingClientRect();
1773
+ if (!rect) return false;
1774
+ if (!menuAllowed(row.task, rect.left + 8, rect.bottom + 4)) return true;
1775
+ return showMenu({
1776
+ kind: "task",
1777
+ task: row.task
1778
+ }, rect.left + 8, rect.bottom + 4);
1779
+ }
1780
+ /** The task the keyboard is on. */
1781
+ const active = ref(null);
1782
+ /** Puts the keyboard on a task: scrolls it into view and focuses its bar, or its table row. */
1783
+ function focusTask(id) {
1784
+ if (!tree.value.byId.get(id)) return;
1785
+ active.value = id;
1786
+ scrollToTask(id);
1787
+ nextTick(() => {
1788
+ (root.value?.querySelector(`.dou-gantt__bar[data-task="${CSS.escape(id)}"]`) ?? root.value?.querySelector(`.dou-gantt__tr[data-row="${CSS.escape(id)}"]`) ?? root.value)?.focus({ preventScroll: true });
1789
+ });
1790
+ }
1791
+ /** Reads a task out as it is once the change has reached the rows. */
1792
+ function announceTask(id, tr) {
1793
+ nextTick(() => {
1794
+ const row = tree.value.byId.get(id);
1795
+ if (!row) return;
1796
+ let text = tr ? withPushed(describe(row), tr) : describe(row);
1797
+ const count = links.value.filter((link) => (link.from === id || link.to === id) && brokenLinks.value.has(link.id)).length;
1798
+ if (count) text = format(messages.value.broken, {
1799
+ task: text,
1800
+ count
1801
+ });
1802
+ announce(text);
1803
+ });
1804
+ }
1805
+ /** Moves the task the keyboard is on (or the selection) by a step, or its end. */
1806
+ function keyMove(direction, which) {
1807
+ const row = active.value ? tree.value.byId.get(active.value) : void 0;
1808
+ if (!row || !editable.value) return;
1809
+ let changes = /* @__PURE__ */ new Map();
1810
+ if (which === "move") {
1811
+ const spans = movers(row);
1812
+ if (!spans.size) return;
1813
+ const unit = stepFor(spans.values()) * direction;
1814
+ const first = spans.get(row.task.id) ?? spans.values().next().value;
1815
+ let shift = unit;
1816
+ for (let i = 0; i < 366 && !isWorkTime(calendar.value, first.start + shift, first.dateOnly); i++) shift += unit;
1817
+ for (const [id, span] of spans) {
1818
+ const start = span.start + shift;
1819
+ changes.set(id, writeSpan(task(id), start, endKeepingWork(calendar.value, span, start)));
1820
+ }
1821
+ } else {
1822
+ const span = row.span;
1823
+ if (!span || row.summary || row.milestone || row.task.locked) return;
1824
+ const unit = stepFor([span]);
1825
+ let end = span.end + unit * direction;
1826
+ while (span.dateOnly && end > span.start + unit && !calendar.value.isWorkday(dayIndex(end - 1))) end += unit * direction;
1827
+ end = Math.max(end, span.start + unit);
1828
+ if (end === span.end) return;
1829
+ changes = /* @__PURE__ */ new Map([[row.task.id, writeSpan(row.task, span.start, end)]]);
1830
+ }
1831
+ const refusal = checkMove(changes);
1832
+ if (refusal) return announce(refusal);
1833
+ const done = transact((tr) => {
1834
+ for (const [id, patch] of changes) tr.updateTask(id, patch);
1835
+ }, { group: `keys:${which}:${row.task.id}` });
1836
+ scrollToTask(row.task.id);
1837
+ announceTask(row.task.id, done);
1838
+ }
1839
+ /** Linking by keys: from the task the keyboard was on, to the one picked with Up and Down. */
1840
+ const keyLink = ref(null);
1841
+ function pickLinkTarget(step) {
1842
+ const link = keyLink.value;
1843
+ const list = rows.value;
1844
+ let index = list.findIndex((row) => row.task.id === (link.to ?? link.from));
1845
+ for (let i = 0; i < list.length; i++) {
1846
+ index = (index + step + list.length) % list.length;
1847
+ const row = list[index];
1848
+ if (row.task.id !== link.from && row.span) break;
1849
+ }
1850
+ const to = list[index].task.id;
1851
+ if (to === link.from) return;
1852
+ keyLink.value = {
1853
+ from: link.from,
1854
+ to,
1855
+ ...checkLink(link.from, to, "fs")
1856
+ };
1857
+ scrollToTask(to);
1858
+ const title = task(to).title;
1859
+ announce(keyLink.value.reason ? `${title},${keyLink.value.reason}` : title);
1860
+ }
1861
+ function onKeyLink(event) {
1862
+ const link = keyLink.value;
1863
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") pickLinkTarget(event.key === "ArrowDown" ? 1 : -1);
1864
+ else if (event.key === "Enter") {
1865
+ if (!link.to) return;
1866
+ if (link.refusal) announce(link.reason ?? messages.value.linkRefusals.cycle);
1867
+ else {
1868
+ const made = {
1869
+ id: createId(),
1870
+ from: link.from,
1871
+ to: link.to
1872
+ };
1873
+ const done = transact((tr) => tr.addLink(made));
1874
+ emit("link", made);
1875
+ announceLinked(made, done);
1876
+ keyLink.value = null;
1877
+ }
1878
+ } else if (event.key === "Escape") {
1879
+ keyLink.value = null;
1880
+ announce(messages.value.cancelled);
1881
+ } else if (event.key !== "Tab") return;
1882
+ event.preventDefault();
1883
+ }
1884
+ const keymaps = computed(() => resolved.value.keymaps.map((keymap) => keymap(context)));
1885
+ function onKeydown(event) {
1886
+ if (event.target.closest("input, textarea, select, [contenteditable]")) return;
1887
+ if (keyLink.value) return onKeyLink(event);
1888
+ for (const keymap of keymaps.value) for (const [hotkey, handler] of Object.entries(keymap)) if (matchesHotkey(event, hotkey) && handler(event) !== false) {
1889
+ event.preventDefault();
1890
+ return;
1891
+ }
1892
+ if (onEditKey(event) || onNavigationKey(event)) event.preventDefault();
1893
+ }
1894
+ /** Undoes or redoes, and says what changed back. */
1895
+ function announceTravel(direction) {
1896
+ const tr = travel(direction);
1897
+ if (!tr) return;
1898
+ const m = messages.value;
1899
+ const step = tr.steps[direction === "undo" ? tr.steps.length - 1 : 0];
1900
+ nextTick(() => {
1901
+ let change = "";
1902
+ if (step.type === "addLink" || step.type === "removeLink") {
1903
+ const link = step.type === "addLink" ? step.link : tr.before.links.find((l) => l.id === step.id);
1904
+ const from = link && tree.value.byId.get(link.from)?.task.title;
1905
+ const to = link && tree.value.byId.get(link.to)?.task.title;
1906
+ change = step.type === "addLink" && from && to ? format(m.linked, {
1907
+ from,
1908
+ to
1909
+ }) : m.linkRemoved;
1910
+ } else if (step.type === "updateLink") change = m.linkChanged;
1911
+ else {
1912
+ const id = step.type === "addTask" ? step.task.id : step.id;
1913
+ const row = tree.value.byId.get(id);
1914
+ change = row ? describe(row) : format(m.taskRemoved, { title: tr.before.tasks.find((t) => t.id === id)?.title ?? "" });
1915
+ }
1916
+ announce(format(m[direction === "undo" ? "undone" : "redone"], { change }));
1917
+ });
1918
+ }
1919
+ function onEditKey(event) {
1920
+ if (!editable.value) return false;
1921
+ if (matchesHotkey(event, "mod+z")) announceTravel("undo");
1922
+ else if (matchesHotkey(event, "mod+shift+z") || matchesHotkey(event, "mod+y")) announceTravel("redo");
1923
+ else if ((event.key === "Delete" || event.key === "Backspace") && selectedLink.value) {
1924
+ const id = selectedLink.value;
1925
+ transact((tr) => tr.removeLink(id));
1926
+ announce(messages.value.linkRemoved);
1927
+ } else if (event.key === "Delete" && selection.value.length) removeTasks(selection.value);
1928
+ else if (event.key === "F2" && active.value) editCell(active.value);
1929
+ else if (matchesHotkey(event, "mod+]") && active.value) indentTask(active.value);
1930
+ else if (matchesHotkey(event, "mod+[") && active.value) outdentTask(active.value);
1931
+ else if (event.key === "ContextMenu" || matchesHotkey(event, "shift+f10")) openKeyMenu();
1932
+ else if (matchesHotkey(event, "alt+arrowleft")) keyMove(-1, "move");
1933
+ else if (matchesHotkey(event, "alt+arrowright")) keyMove(1, "move");
1934
+ else if (matchesHotkey(event, "alt+shift+arrowleft")) keyMove(-1, "end");
1935
+ else if (matchesHotkey(event, "alt+shift+arrowright")) keyMove(1, "end");
1936
+ else if (matchesHotkey(event, "l") && active.value) {
1937
+ keyLink.value = {
1938
+ from: active.value,
1939
+ to: null,
1940
+ refusal: null
1941
+ };
1942
+ announce(format(messages.value.linkMode, { title: task(active.value).title }));
1943
+ } else return false;
1944
+ return true;
1945
+ }
1946
+ function onNavigationKey(event) {
1947
+ if (event.altKey || event.metaKey || event.ctrlKey) return false;
1948
+ const list = rows.value;
1949
+ if (!list.length) return false;
1950
+ const row = active.value ? tree.value.byId.get(active.value) : void 0;
1951
+ const index = row && row.index >= 0 ? row.index : -1;
1952
+ const go = (i) => focusTask(list[Math.max(0, Math.min(list.length - 1, i))].task.id);
1953
+ switch (event.key) {
1954
+ case "ArrowDown":
1955
+ go(index < 0 ? 0 : index + 1);
1956
+ break;
1957
+ case "ArrowUp":
1958
+ go(index < 0 ? 0 : index - 1);
1959
+ break;
1960
+ case "Home":
1961
+ go(0);
1962
+ break;
1963
+ case "End":
1964
+ go(list.length - 1);
1965
+ break;
1966
+ case "ArrowRight":
1967
+ if (!row) return false;
1968
+ if (row.summary && row.task.collapsed) toggle(row);
1969
+ else if (row.summary) go(index + 1);
1970
+ break;
1971
+ case "ArrowLeft":
1972
+ if (!row) return false;
1973
+ if (row.summary && !row.task.collapsed) toggle(row);
1974
+ else if (row.task.parent != null && tree.value.byId.has(row.task.parent)) focusTask(row.task.parent);
1975
+ break;
1976
+ case " ":
1977
+ case "Enter":
1978
+ if (!row) return false;
1979
+ selectedLink.value = null;
1980
+ if (event.key === "Enter") selection.value = [row.task.id];
1981
+ else selection.value = selected.value.has(row.task.id) ? selection.value.filter((id) => id !== row.task.id) : [...selection.value, row.task.id];
1982
+ break;
1983
+ default: return false;
1984
+ }
1985
+ return true;
1986
+ }
1987
+ const live = ref("");
1988
+ /** Set while a cancelled change is being told of, so what follows doesn't say it was made. */
1989
+ let refusing = false;
1990
+ function announce(text) {
1991
+ if (refusing) return;
1992
+ live.value = "";
1993
+ nextTick(() => live.value = text);
1994
+ }
1995
+ /** Why the last change didn't go ahead, shown for a moment over the editor. */
1996
+ const notice = ref("");
1997
+ let noticeTimer;
1998
+ onBeforeUnmount(() => clearTimeout(noticeTimer));
1999
+ /** Says why a change didn't go ahead, once while the same one repeats, as in a drag. */
2000
+ let lastRefusal = {
2001
+ text: "",
2002
+ at: 0
2003
+ };
2004
+ function refuse(text) {
2005
+ const at = Date.now();
2006
+ if (refusing || text === lastRefusal.text && at - lastRefusal.at < 1e3) return;
2007
+ lastRefusal = {
2008
+ text,
2009
+ at
2010
+ };
2011
+ announce(text);
2012
+ notice.value = text;
2013
+ clearTimeout(noticeTimer);
2014
+ noticeTimer = setTimeout(() => notice.value = "", 3200);
2015
+ refusing = true;
2016
+ nextTick(() => refusing = false);
2017
+ }
2018
+ const keysHint = `${useId()}-keys`;
2019
+ const markers = computed(() => {
2020
+ const inRange = [...props.markers ?? [], ...resolved.value.markers.flatMap((fromExtension) => fromExtension(context))].flatMap((marker, index) => {
2021
+ const at = parseTime(marker.date)?.at;
2022
+ if (at == null || at < extent.value.start || at > extent.value.end) return [];
2023
+ return [{
2024
+ key: `${index}:${marker.date}`,
2025
+ x: xOf(at),
2026
+ marker
2027
+ }];
2028
+ });
2029
+ const room = markerRoom(inRange.map((item) => item.marker.label ? item.x : -Infinity));
2030
+ return inRange.map((item, i) => ({
2031
+ ...item,
2032
+ room: room[i]
2033
+ }));
2034
+ });
2035
+ /** The widest drawing made, in px; wider ones are zoomed out to fit. */
2036
+ const PAGE = {
2037
+ landscape: [1047, 718],
2038
+ portrait: [718, 1047]
2039
+ };
2040
+ /** The columns of the table as drawn, at their widths on screen. */
2041
+ function exportColumns() {
2042
+ const heads = root.value?.querySelectorAll(".dou-gantt__th");
2043
+ return columns.value.map((column, i) => ({
2044
+ title: headerOf(column),
2045
+ width: heads?.[i]?.offsetWidth || px(column.width) || (column.key === "title" ? 200 : 96),
2046
+ align: column.align ?? "start",
2047
+ isTitle: column.key === "title",
2048
+ text: (row) => cellText(row, column)
2049
+ }));
2050
+ }
2051
+ let measurer;
2052
+ function exportTextWidth(theme) {
2053
+ if (measurer === void 0) try {
2054
+ measurer = document.createElement("canvas").getContext("2d");
2055
+ } catch {
2056
+ measurer = null;
2057
+ }
2058
+ return (text, bold = false) => {
2059
+ if (!measurer) return text.length * theme.fontSize;
2060
+ measurer.font = `${bold ? theme.fontWeight : 400} ${theme.fontSize}px ${theme.fontFamily}`;
2061
+ return measurer.measureText(text).width;
2062
+ };
2063
+ }
2064
+ function drawChart(options) {
2065
+ const theme = readTheme(root.value);
2066
+ let width = options.dayWidth ?? dayWidth.value;
2067
+ let shownRows = rows.value;
2068
+ let start;
2069
+ let end;
2070
+ if (options.range === "view") {
2071
+ start = origin.value + scrollLeft.value / ppm.value;
2072
+ end = start + (viewWidth.value || 800) / ppm.value;
2073
+ const top = scrollTop.value;
2074
+ const bottom = top + bodyHeight.value;
2075
+ shownRows = rows.value.filter((row) => (row.index + 1) * props.rowHeight > top && row.index * props.rowHeight < bottom);
2076
+ } else {
2077
+ const known = content.value ?? {
2078
+ start: now.value,
2079
+ end: now.value + 1440
2080
+ };
2081
+ if ((known.end - known.start) * (width / 1440) > EXPORT_MAX) width = EXPORT_MAX / (known.end - known.start) * MINUTES_PER_DAY;
2082
+ ({start, end} = paddedRange(known.start, known.end, width, weekStart.value));
2083
+ const pxPerMinute = width / MINUTES_PER_DAY;
2084
+ const measure = exportTextWidth(theme);
2085
+ let right = known.end;
2086
+ for (const row of rows.value) {
2087
+ if (!row.span || props.barLabel === "none") continue;
2088
+ const barWidth = (row.span.end - row.span.start) * pxPerMinute;
2089
+ if (row.milestone || row.summary || props.barLabel === "outside" || props.barLabel === "auto" && measure(row.task.title) + 20 > barWidth) right = Math.max(right, row.span.end + (measure(row.task.title) + 24 + 14) / pxPerMinute);
2090
+ }
2091
+ if (right > end) end = paddedRange(known.start, right, width, weekStart.value).end;
2092
+ }
2093
+ return drawGantt({
2094
+ rows: shownRows,
2095
+ links: linkList.value,
2096
+ columns: options.table ? exportColumns() : [],
2097
+ rowHeight: props.rowHeight,
2098
+ dayWidth: width,
2099
+ start,
2100
+ end,
2101
+ calendar: calendar.value,
2102
+ messages: messages.value,
2103
+ weekStart: weekStart.value,
2104
+ now: props.today ? now.value : null,
2105
+ markers: markers.value.map((item) => ({
2106
+ at: parseTime(item.marker.date).at,
2107
+ label: item.marker.label,
2108
+ tone: item.marker.tone
2109
+ })),
2110
+ critical: critical.value,
2111
+ broken: brokenLinks.value,
2112
+ baseline: planned.value,
2113
+ barLabel: props.barLabel,
2114
+ background: options.background !== false,
2115
+ linkedEnds: linkedEnds.value,
2116
+ textWidth: exportTextWidth(theme)
2117
+ }, theme);
2118
+ }
2119
+ const exportOptions = (options = {}) => ({
2120
+ range: options.range ?? "all",
2121
+ table: options.table ?? showTable.value,
2122
+ dayWidth: options.scale ? levelByName(options.scale).base : void 0,
2123
+ background: options.background
2124
+ });
2125
+ function exportSVG(options) {
2126
+ return svgDocument(drawChart(exportOptions(options)));
2127
+ }
2128
+ async function exportPNG(options = {}) {
2129
+ const drawing = drawChart(exportOptions(options));
2130
+ return svgToPNG(svgDocument(drawing), {
2131
+ width: drawing.width,
2132
+ height: 56 + drawing.bodyHeight,
2133
+ pixelRatio: options.pixelRatio,
2134
+ scope: "@dou-ui/gantt",
2135
+ hint: "export range 'view', a coarser scale, or use exportSVG() / toCSV()"
2136
+ });
2137
+ }
2138
+ /** What a spreadsheet program would start a formula with; kept in step with `@dou-ui/sheet`. */
2139
+ const FORMULA_START = /^[=+\-@\t\r]/;
2140
+ /**
2141
+ * A field of CSV: quoted when it has to be, and unless `escape` is off,
2142
+ * text that a spreadsheet program would run as a formula (`=…`, `-复盘`)
2143
+ * behind a `'`. Numbers stay as they are.
2144
+ */
2145
+ function csvCell(text, escape = true) {
2146
+ const safe = escape && FORMULA_START.test(text) && !Number.isFinite(Number(text.trim())) ? `'${text}` : text;
2147
+ return /[",\n\r]/.test(safe) ? `"${safe.replace(/"/g, "\"\"")}"` : safe;
2148
+ }
2149
+ /** A cell as a spreadsheet reads it: dates as `YYYY-MM-DD`, numbers without units. */
2150
+ function csvValue(row, column, mixed) {
2151
+ if (column.value) {
2152
+ const value = column.value(row.task);
2153
+ if (value == null) return "";
2154
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
2155
+ }
2156
+ const span = row.span;
2157
+ if (column.key === "start") return span ? formatTime(span.start - (row.milestone && span.dateOnly ? MINUTES_PER_DAY / 2 : 0), span.dateOnly) : "";
2158
+ if (row.milestone && [
2159
+ "end",
2160
+ "duration",
2161
+ "progress"
2162
+ ].includes(column.key)) return "";
2163
+ if (column.key === "duration" && mixed && span) return durationLabel(span, calendar.value, messages.value);
2164
+ return editorValue(row, column);
2165
+ }
2166
+ function toCSV(options = {}) {
2167
+ const list = columns.value;
2168
+ const m = messages.value;
2169
+ const timed = /* @__PURE__ */ new Set();
2170
+ for (const row of tree.value.byId.values()) if (row.span && !row.milestone) timed.add(!row.span.dateOnly);
2171
+ const mixed = timed.size > 1;
2172
+ const header = (column) => {
2173
+ const title = headerOf(column);
2174
+ if (options.text || column.value || column.key !== "duration" || timed.size !== 1) return title;
2175
+ return format(timed.has(true) ? m.hoursColumn : m.daysColumn, { title });
2176
+ };
2177
+ const cell = (row, column) => options.text ? cellText(row, column) : csvValue(row, column, mixed);
2178
+ const ids = options.ids ? [m.idColumn, m.parentColumn] : [];
2179
+ const escape = options.escapeFormulas !== false;
2180
+ const field = (text) => csvCell(text, escape);
2181
+ const lines = [[...ids, ...list.map(header)].map(field).join(",")];
2182
+ for (const row of tree.value.byId.values()) {
2183
+ const own = options.ids ? [row.task.id, row.task.parent ?? ""] : [];
2184
+ lines.push([...own, ...list.map((column) => cell(row, column))].map(field).join(","));
2185
+ }
2186
+ return (options.bom === false ? "" : "") + lines.join("\r\n");
2187
+ }
2188
+ function readCSV(text, outer) {
2189
+ return readGanttCSV(text, {
2190
+ columns: columns.value,
2191
+ messages: messages.value,
2192
+ calendar: calendar.value,
2193
+ outer
2194
+ });
2195
+ }
2196
+ function importCSV(text, options = {}) {
2197
+ if (!editable.value) return null;
2198
+ const mode = options.mode ?? "replace";
2199
+ const result = readCSV(text, mode === "append" ? (id) => tree.value.byId.has(id) : void 0);
2200
+ transact((tr) => applyGanttCSV(tr, result, mode, calendar.value));
2201
+ return result;
2202
+ }
2203
+ const escapeHTML = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2204
+ function print(options = {}) {
2205
+ const orientation = options.orientation ?? "landscape";
2206
+ const [pageWidth, pageHeight] = PAGE[orientation];
2207
+ const table = options.table ?? true;
2208
+ const columnsWidth = table ? exportColumns().reduce((sum, column) => sum + column.width, 0) : 0;
2209
+ const known = content.value ?? {
2210
+ start: now.value,
2211
+ end: now.value + 1440
2212
+ };
2213
+ let fitted = (pageWidth - columnsWidth - 48) / (known.end - known.start) * MINUTES_PER_DAY;
2214
+ fitted = Math.min(DAY_WIDTH_RANGE[1], Math.max(DAY_WIDTH_RANGE[0], fitted));
2215
+ const drawing = drawChart({
2216
+ range: "all",
2217
+ table,
2218
+ dayWidth: fitted
2219
+ });
2220
+ const perPage = Math.max(1, Math.floor((pageHeight * Math.max(1, drawing.width / pageWidth) - 56) / props.rowHeight));
2221
+ const pages = [];
2222
+ for (let top = 0; top < Math.max(1, drawing.bodyHeight); top += perPage * props.rowHeight) pages.push(`<div class="page">${svgDocument(drawing, {
2223
+ top,
2224
+ height: Math.min(perPage * props.rowHeight, drawing.bodyHeight - top)
2225
+ })}</div>`);
2226
+ const iframe = document.createElement("iframe");
2227
+ iframe.setAttribute("aria-hidden", "true");
2228
+ iframe.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
2229
+ document.body.append(iframe);
2230
+ const doc = iframe.contentDocument;
2231
+ doc.open();
2232
+ doc.write(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHTML(document.title)}</title><style>@page{size:A4 ${orientation};margin:10mm}html,body{margin:0}.page{break-after:page;print-color-adjust:exact;-webkit-print-color-adjust:exact}.page:last-child{break-after:auto}svg{display:block;width:100%;height:auto}</style></head><body>${pages.join("")}</body></html>`);
2233
+ doc.close();
2234
+ const win = iframe.contentWindow;
2235
+ const done = () => iframe.remove();
2236
+ win.addEventListener("afterprint", done);
2237
+ setTimeout(done, 6e4);
2238
+ win.focus();
2239
+ win.print();
2240
+ }
2241
+ function onLinkClick(link, event) {
2242
+ selectedLink.value = link.id;
2243
+ root.value?.focus({ preventScroll: true });
2244
+ emit("linkClick", link, event);
2245
+ }
2246
+ function scrollToTask(id) {
2247
+ const row = tree.value.byId.get(id);
2248
+ if (!row) return;
2249
+ const el = chart.value;
2250
+ if (el && row.index >= 0) {
2251
+ const top = row.index * props.rowHeight;
2252
+ if (top < el.scrollTop) el.scrollTop = top;
2253
+ else if (top + props.rowHeight > el.scrollTop + bodyHeight.value) el.scrollTop = top + props.rowHeight - bodyHeight.value;
2254
+ }
2255
+ if (row.span) {
2256
+ const left = xOf(row.span.start);
2257
+ const right = xOf(row.span.end);
2258
+ if (left < scrollLeft.value || right > scrollLeft.value + viewWidth.value) scrollToTime(row.span.start);
2259
+ }
2260
+ }
2261
+ __expose({
2262
+ transact,
2263
+ undo,
2264
+ redo,
2265
+ schedule,
2266
+ editCell,
2267
+ insertTask,
2268
+ indentTask,
2269
+ outdentTask,
2270
+ get canUndo() {
2271
+ return canUndo.value;
2272
+ },
2273
+ get canRedo() {
2274
+ return canRedo.value;
2275
+ },
2276
+ fit,
2277
+ scrollToDate,
2278
+ scrollToTask,
2279
+ scrollToToday,
2280
+ focusTask,
2281
+ zoomIn,
2282
+ zoomOut,
2283
+ exportSVG,
2284
+ exportPNG,
2285
+ toCSV,
2286
+ readCSV: (text) => readCSV(text),
2287
+ importCSV,
2288
+ print
2289
+ });
2290
+ const context = {
2291
+ doc: source,
2292
+ tasks,
2293
+ links,
2294
+ selection,
2295
+ active,
2296
+ readonly: computed(() => !editable.value),
2297
+ messages,
2298
+ scale: computed(() => level.value.name),
2299
+ dayWidth,
2300
+ rowHeight: computed(() => props.rowHeight),
2301
+ rows,
2302
+ preview,
2303
+ critical,
2304
+ brokenLinks,
2305
+ schedule,
2306
+ editCell,
2307
+ insertTask,
2308
+ indentTask,
2309
+ outdentTask,
2310
+ transact,
2311
+ undo,
2312
+ redo,
2313
+ canUndo,
2314
+ canRedo,
2315
+ fit,
2316
+ zoomIn,
2317
+ zoomOut,
2318
+ scrollToDate,
2319
+ scrollToTask,
2320
+ scrollToToday,
2321
+ focusTask,
2322
+ xOf: (time) => {
2323
+ const at = typeof time === "number" ? time : parseTime(time)?.at;
2324
+ return at == null ? null : xOf(at);
2325
+ },
2326
+ rowTop: (id) => {
2327
+ const row = tree.value.byId.get(id);
2328
+ return row && row.index >= 0 ? row.index * props.rowHeight : null;
2329
+ },
2330
+ announce
2331
+ };
2332
+ provide(ganttKey, context);
2333
+ let cleanups = [];
2334
+ let mounted = false;
2335
+ function teardownExtensions() {
2336
+ for (const cleanup of cleanups.splice(0)) cleanup();
2337
+ customHistory = null;
2338
+ }
2339
+ let setUp = false;
2340
+ let setupTimes = [];
2341
+ let setupWarned = false;
2342
+ /** An extension made in the template is new on every render, and set up again each time. */
2343
+ function warnRepeatedSetup() {
2344
+ if (process.env.NODE_ENV === "production" || setupWarned) return;
2345
+ const at = Date.now();
2346
+ setupTimes = [...setupTimes.filter((time) => at - time < 1e3), at];
2347
+ if (setupTimes.length < 4) return;
2348
+ setupWarned = true;
2349
+ console.warn("[@dou-ui/gantt] the extensions keep being set up again, which also clears undo each time. Create them once in <script setup> instead of in the template.");
2350
+ }
2351
+ function setupExtensions() {
2352
+ if (setUp) warnRepeatedSetup();
2353
+ setUp = true;
2354
+ teardownExtensions();
2355
+ customHistory = resolved.value.history?.(context) ?? null;
2356
+ for (const setup of resolved.value.setups) {
2357
+ const cleanup = setup(context);
2358
+ if (typeof cleanup === "function") cleanups.push(cleanup);
2359
+ }
2360
+ syncHistory();
2361
+ }
2362
+ onMounted(() => {
2363
+ mounted = true;
2364
+ setupExtensions();
2365
+ });
2366
+ watch(() => [resolved.value.history, ...resolved.value.setups], (hooks, before) => {
2367
+ if (mounted && (hooks.length !== before.length || hooks.some((hook, i) => hook !== before[i]))) setupExtensions();
2368
+ });
2369
+ onBeforeUnmount(teardownExtensions);
2370
+ const rowStyle = (row) => ({ transform: `translateY(${row.index * props.rowHeight}px)` });
2371
+ return (_ctx, _cache) => {
2372
+ return openBlock(), createElementBlock("div", {
2373
+ ref_key: "root",
2374
+ ref: root,
2375
+ class: normalizeClass(["dou-gantt", {
2376
+ "is-narrow": narrow.value,
2377
+ "is-editable": editable.value,
2378
+ "is-dragging": drag.value?.started || rowDrag.value?.started,
2379
+ "is-dragging-row": rowDrag.value?.started,
2380
+ [`is-dragging-${drag.value?.mode}`]: drag.value?.started,
2381
+ "is-linking": !!keyLink.value,
2382
+ "has-baseline": !!planned.value
2383
+ }]),
2384
+ style: normalizeStyle({ "--dou-gantt-row-height": `${__props.rowHeight}px` }),
2385
+ role: "group",
2386
+ tabindex: "0",
2387
+ "aria-label": __props.ariaLabel ?? unref(messages).chart,
2388
+ "aria-describedby": keysHint,
2389
+ onKeydown,
2390
+ onPointerdownCapture: onRootPointerdown
2391
+ }, [
2392
+ createElementVNode("span", {
2393
+ id: keysHint,
2394
+ class: "dou-gantt__sr-only"
2395
+ }, toDisplayString(unref(messages).keys), 1),
2396
+ createElementVNode("div", _hoisted_2, toDisplayString(live.value), 1),
2397
+ notice.value ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString(notice.value), 1)) : createCommentVNode("", true),
2398
+ showTable.value ? (openBlock(), createElementBlock("div", {
2399
+ key: 1,
2400
+ ref_key: "tablePane",
2401
+ ref: tablePane,
2402
+ class: "dou-gantt__table-pane",
2403
+ style: normalizeStyle({ width: `${tableSize.value}px` })
2404
+ }, [createElementVNode("div", {
2405
+ class: "dou-gantt__table",
2406
+ role: "treegrid",
2407
+ "aria-rowcount": rows.value.length,
2408
+ style: normalizeStyle({ minWidth: `${tableMinWidth.value}px` })
2409
+ }, [createElementVNode("div", _hoisted_5, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column) => {
2410
+ return openBlock(), createElementBlock("div", {
2411
+ key: column.key,
2412
+ class: normalizeClass(["dou-gantt__th", [column.align && `is-${column.align}`, { "is-title": column.key === "title" }]]),
2413
+ style: normalizeStyle(columnStyle(column)),
2414
+ role: "columnheader"
2415
+ }, toDisplayString(headerOf(column)), 7);
2416
+ }), 128))]), createElementVNode("div", {
2417
+ ref_key: "tableBody",
2418
+ ref: tableBody,
2419
+ class: "dou-gantt__table-body",
2420
+ onScroll: _cache[1] || (_cache[1] = (...args) => unref(onTableScroll) && unref(onTableScroll)(...args))
2421
+ }, [createElementVNode("div", {
2422
+ class: "dou-gantt__table-rows",
2423
+ style: normalizeStyle({ height: `${bodyRows.value * __props.rowHeight + Math.max(0, unref(scrollbar) - tableScrollbar.value)}px` })
2424
+ }, [
2425
+ (openBlock(true), createElementBlock(Fragment, null, renderList(visibleRows.value, (row) => {
2426
+ return openBlock(), createElementBlock("div", {
2427
+ key: row.task.id,
2428
+ class: normalizeClass(["dou-gantt__tr", {
2429
+ "is-selected": selected.value.has(row.task.id),
2430
+ "is-hovered": hovered.value === row.task.id,
2431
+ "is-summary": row.summary,
2432
+ "is-active": active.value === row.task.id,
2433
+ "is-moving": rowDragged.value?.has(row.task.id),
2434
+ "is-drop-inside": rowDrop.value?.row === row && rowDrop.value.where === "inside"
2435
+ }]),
2436
+ style: normalizeStyle(rowStyle(row)),
2437
+ "data-row": row.task.id,
2438
+ tabindex: "-1",
2439
+ role: "row",
2440
+ "aria-level": row.depth + 1,
2441
+ "aria-rowindex": row.index + 1,
2442
+ "aria-expanded": row.summary ? !row.task.collapsed : void 0,
2443
+ "aria-selected": selected.value.has(row.task.id),
2444
+ onPointerenter: ($event) => hovered.value = row.task.id,
2445
+ onPointerleave: _cache[0] || (_cache[0] = ($event) => hovered.value = null),
2446
+ onPointerdown: ($event) => onRowDown($event, row),
2447
+ onClick: ($event) => onTaskClick(row, $event),
2448
+ onDblclick: ($event) => emit("taskDblclick", row.task, $event),
2449
+ onContextmenu: ($event) => onTaskContextmenu(row, $event)
2450
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column) => {
2451
+ return openBlock(), createElementBlock("div", {
2452
+ key: column.key,
2453
+ class: normalizeClass(["dou-gantt__td", [column.align && `is-${column.align}`, { "is-title": column.key === "title" }]]),
2454
+ style: normalizeStyle(columnStyle(column)),
2455
+ role: "gridcell",
2456
+ onDblclick: ($event) => onCellDblclick(row, column, $event)
2457
+ }, [column.key === "title" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", {
2458
+ class: "dou-gantt__indent",
2459
+ style: normalizeStyle({ width: `${row.depth * 16}px` })
2460
+ }, null, 4), row.summary ? (openBlock(), createElementBlock("button", {
2461
+ key: 0,
2462
+ type: "button",
2463
+ class: "dou-gantt__toggle",
2464
+ tabindex: "-1",
2465
+ "aria-label": row.task.collapsed ? unref(messages).expand : unref(messages).collapse,
2466
+ onClick: withModifiers(($event) => toggle(row), ["stop"])
2467
+ }, [(openBlock(), createBlock(resolveDynamicComponent(row.task.collapsed ? unref(ChevronRightIcon) : unref(ChevronDownIcon))))], 8, _hoisted_8)) : (openBlock(), createElementBlock("span", _hoisted_9))], 64)) : createCommentVNode("", true), editing.value?.id === row.task.id && editing.value.key === column.key ? (openBlock(), createBlock(cell_editor_default, {
2468
+ key: 1,
2469
+ kind: editorKind(row, column),
2470
+ value: editorValue(row, column),
2471
+ label: unref(format)(unref(messages).editCell, { column: headerOf(column) }),
2472
+ check: (text) => editPatch(row, column, text) !== false,
2473
+ onDone: ($event) => onEditDone(row, column, $event),
2474
+ onInvalid: ($event) => onEditInvalid(column),
2475
+ onTab: ($event) => onEditTab(row, $event)
2476
+ }, null, 8, [
2477
+ "kind",
2478
+ "value",
2479
+ "label",
2480
+ "check",
2481
+ "onDone",
2482
+ "onInvalid",
2483
+ "onTab"
2484
+ ])) : (openBlock(), createElementBlock("span", _hoisted_10, [renderSlot(_ctx.$slots, "cell", {
2485
+ task: row.task,
2486
+ row,
2487
+ column,
2488
+ text: cellText(row, column)
2489
+ }, () => [createTextVNode(toDisplayString(cellText(row, column)), 1)])]))], 46, _hoisted_7);
2490
+ }), 128))], 46, _hoisted_6);
2491
+ }), 128)),
2492
+ showAddRow.value ? (openBlock(), createElementBlock("div", {
2493
+ key: 0,
2494
+ class: "dou-gantt__add-row",
2495
+ style: normalizeStyle({ transform: `translateY(${rows.value.length * __props.rowHeight}px)` }),
2496
+ role: "row"
2497
+ }, [createElementVNode("div", _hoisted_11, [_cache[6] || (_cache[6] = createElementVNode("span", { class: "dou-gantt__toggle-space" }, null, -1)), editing.value && editing.value.id === null ? (openBlock(), createBlock(cell_editor_default, {
2498
+ key: 0,
2499
+ kind: "text",
2500
+ value: "",
2501
+ repeat: "",
2502
+ label: unref(messages).addRow,
2503
+ placeholder: unref(messages).addRow,
2504
+ onDone: onAddDone
2505
+ }, null, 8, ["label", "placeholder"])) : (openBlock(), createElementBlock("button", {
2506
+ key: 1,
2507
+ type: "button",
2508
+ class: "dou-gantt__add",
2509
+ onClick: startAdding
2510
+ }, [createVNode(unref(PlusIcon)), createTextVNode(" " + toDisplayString(unref(messages).addRow), 1)]))])], 4)) : createCommentVNode("", true),
2511
+ dropLine.value ? (openBlock(), createElementBlock("div", {
2512
+ key: 1,
2513
+ class: "dou-gantt__drop-line",
2514
+ style: normalizeStyle(dropLine.value)
2515
+ }, null, 4)) : createCommentVNode("", true)
2516
+ ], 4)], 544)], 12, _hoisted_4)], 4)) : createCommentVNode("", true),
2517
+ createElementVNode("div", {
2518
+ ref_key: "chart",
2519
+ ref: chart,
2520
+ class: normalizeClass(["dou-gantt__chart", { "is-fixed": __props.range }]),
2521
+ tabindex: "-1",
2522
+ onScrollPassive: _cache[3] || (_cache[3] = (...args) => unref(onChartScroll) && unref(onChartScroll)(...args)),
2523
+ onWheel: _cache[4] || (_cache[4] = (...args) => unref(onWheel) && unref(onWheel)(...args))
2524
+ }, [createElementVNode("div", {
2525
+ class: "dou-gantt__canvas",
2526
+ style: normalizeStyle({ width: `${unref(totalWidth)}px` }),
2527
+ onContextmenu: onCanvasContextmenu
2528
+ }, [createVNode(time_header_default, {
2529
+ "top-cells": unref(topCells),
2530
+ "bottom-cells": unref(bottomCells),
2531
+ "bottom-unit": unref(level).bottom,
2532
+ calendar: calendar.value,
2533
+ now: unref(now),
2534
+ "today-x": unref(todayX)
2535
+ }, {
2536
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(markers.value, (item) => {
2537
+ return openBlock(), createElementBlock("span", {
2538
+ key: item.key,
2539
+ class: normalizeClass(["dou-gantt__marker-label", [item.marker.tone && `is-${item.marker.tone}`, {
2540
+ "is-cut": item.room !== Infinity,
2541
+ "is-crowded": !item.room
2542
+ }]]),
2543
+ title: item.room !== Infinity ? item.marker.label : void 0,
2544
+ style: normalizeStyle({
2545
+ left: `${item.x}px`,
2546
+ maxWidth: item.room && item.room !== Infinity ? `${item.room}px` : void 0
2547
+ })
2548
+ }, [item.marker.label ? (openBlock(), createElementBlock("span", _hoisted_13, toDisplayString(item.marker.label), 1)) : createCommentVNode("", true)], 14, _hoisted_12);
2549
+ }), 128))]),
2550
+ _: 1
2551
+ }, 8, [
2552
+ "top-cells",
2553
+ "bottom-cells",
2554
+ "bottom-unit",
2555
+ "calendar",
2556
+ "now",
2557
+ "today-x"
2558
+ ]), createElementVNode("div", {
2559
+ class: "dou-gantt__body",
2560
+ style: normalizeStyle({ height: `${bodyRows.value * __props.rowHeight}px` }),
2561
+ onPointerdown: onBodyDown,
2562
+ onContextmenu: onBodyContextmenu
2563
+ }, [
2564
+ !showTable.value ? (openBlock(), createElementBlock("div", _hoisted_14, [(openBlock(true), createElementBlock(Fragment, null, renderList(rowTitles.value, (item) => {
2565
+ return openBlock(), createElementBlock("span", {
2566
+ key: item.row.task.id,
2567
+ class: normalizeClass(["dou-gantt__row-title", { "is-summary": item.row.summary }]),
2568
+ style: normalizeStyle(rowStyle(item.row))
2569
+ }, toDisplayString(item.row.task.title), 7);
2570
+ }), 128))])) : createCommentVNode("", true),
2571
+ createVNode(time_grid_default, {
2572
+ cells: unref(bottomCells),
2573
+ shaded: unref(shaded)
2574
+ }, null, 8, ["cells", "shaded"]),
2575
+ (openBlock(true), createElementBlock(Fragment, null, renderList(highlightRows.value, (row) => {
2576
+ return openBlock(), createElementBlock("div", {
2577
+ key: row.task.id,
2578
+ class: normalizeClass(["dou-gantt__row-highlight", { "is-selected": selected.value.has(row.task.id) }]),
2579
+ style: normalizeStyle(rowStyle(row))
2580
+ }, null, 6);
2581
+ }), 128)),
2582
+ unref(todayX) != null ? (openBlock(), createElementBlock("div", {
2583
+ key: 1,
2584
+ class: "dou-gantt__today",
2585
+ style: normalizeStyle({ left: `${unref(todayX)}px` })
2586
+ }, null, 4)) : createCommentVNode("", true),
2587
+ (openBlock(true), createElementBlock(Fragment, null, renderList(markers.value, (item) => {
2588
+ return openBlock(), createElementBlock("div", {
2589
+ key: item.key,
2590
+ class: normalizeClass(["dou-gantt__marker", item.marker.tone && `is-${item.marker.tone}`]),
2591
+ style: normalizeStyle({ left: `${item.x}px` })
2592
+ }, null, 6);
2593
+ }), 128)),
2594
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.below, (layer, i) => {
2595
+ return openBlock(), createBlock(resolveDynamicComponent(layer), { key: `below-${i}` });
2596
+ }), 128)),
2597
+ (openBlock(), createElementBlock("svg", {
2598
+ class: "dou-gantt__links",
2599
+ width: unref(totalWidth),
2600
+ height: rows.value.length * __props.rowHeight
2601
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(linkPaths.value, (item) => {
2602
+ return openBlock(), createElementBlock("g", {
2603
+ key: item.link.id,
2604
+ class: normalizeClass(["dou-gantt__link", {
2605
+ "is-related": selected.value.has(item.link.from) || selected.value.has(item.link.to),
2606
+ "is-selected": selectedLink.value === item.link.id,
2607
+ "is-broken": brokenLinks.value.has(item.link.id),
2608
+ "is-critical": critical.value?.links.has(item.link.id)
2609
+ }]),
2610
+ onClick: ($event) => onLinkClick(item.link, $event),
2611
+ onContextmenu: ($event) => onLinkContextmenu(item.link, $event)
2612
+ }, [
2613
+ createElementVNode("path", {
2614
+ class: "dou-gantt__link-hit",
2615
+ d: item.d
2616
+ }, null, 8, _hoisted_17),
2617
+ createElementVNode("path", {
2618
+ class: "dou-gantt__link-line",
2619
+ d: item.d
2620
+ }, null, 8, _hoisted_18),
2621
+ createElementVNode("path", {
2622
+ class: "dou-gantt__link-arrow",
2623
+ d: item.arrow
2624
+ }, null, 8, _hoisted_19)
2625
+ ], 42, _hoisted_16);
2626
+ }), 128)), draftLink.value ? (openBlock(), createElementBlock("path", {
2627
+ key: 0,
2628
+ class: normalizeClass(["dou-gantt__link-draft", { "is-refused": draftLink.value.refused }]),
2629
+ d: draftLink.value.d
2630
+ }, null, 10, _hoisted_20)) : createCommentVNode("", true)], 8, _hoisted_15)),
2631
+ (openBlock(true), createElementBlock(Fragment, null, renderList(baselineBars.value, (plan) => {
2632
+ return openBlock(), createElementBlock("div", {
2633
+ key: `plan:${plan.id}`,
2634
+ class: normalizeClass(["dou-gantt__baseline", { "is-milestone": plan.milestone }]),
2635
+ style: normalizeStyle({
2636
+ transform: `translate(${plan.left}px, ${plan.top}px)${plan.milestone ? " rotate(45deg)" : ""}`,
2637
+ width: plan.milestone ? void 0 : `${plan.width}px`
2638
+ })
2639
+ }, null, 6);
2640
+ }), 128)),
2641
+ (openBlock(true), createElementBlock(Fragment, null, renderList(bars.value, (bar) => {
2642
+ return openBlock(), createElementBlock("div", {
2643
+ key: bar.row.task.id,
2644
+ class: normalizeClass(["dou-gantt__bar-row", {
2645
+ "is-selected": selected.value.has(bar.row.task.id),
2646
+ "is-active": drag.value?.started && drag.value.id === bar.row.task.id
2647
+ }]),
2648
+ style: normalizeStyle({ transform: `translateY(${bar.top}px)` }),
2649
+ onPointerenter: ($event) => hovered.value = bar.row.task.id,
2650
+ onPointerleave: _cache[2] || (_cache[2] = ($event) => hovered.value = null)
2651
+ }, [
2652
+ createElementVNode("div", {
2653
+ class: normalizeClass(["dou-gantt__bar", [bar.row.task.tone && `is-${bar.row.task.tone}`, {
2654
+ "is-summary": bar.row.summary,
2655
+ "is-milestone": bar.row.milestone,
2656
+ "is-selected": selected.value.has(bar.row.task.id),
2657
+ "is-locked": bar.row.task.locked,
2658
+ "is-link-target": linkHighlight.value?.id === bar.row.task.id && !linkHighlight.value.refused,
2659
+ "is-link-refused": linkHighlight.value?.id === bar.row.task.id && linkHighlight.value.refused,
2660
+ "is-move-refused": !!dragRefusal.value && drag.value?.started && drag.value.spans.has(bar.row.task.id),
2661
+ "is-active": active.value === bar.row.task.id,
2662
+ "is-critical": critical.value?.tasks.has(bar.row.task.id)
2663
+ }]]),
2664
+ "data-task": bar.row.task.id,
2665
+ style: normalizeStyle({
2666
+ left: `${bar.left}px`,
2667
+ width: bar.row.milestone ? void 0 : `${bar.width}px`
2668
+ }),
2669
+ role: "img",
2670
+ tabindex: "-1",
2671
+ "aria-label": bar.aria,
2672
+ onPointerdown: ($event) => onBarDown($event, bar.row),
2673
+ onClick: ($event) => onTaskClick(bar.row, $event),
2674
+ onDblclick: ($event) => emit("taskDblclick", bar.row.task, $event),
2675
+ onContextmenu: ($event) => onTaskContextmenu(bar.row, $event)
2676
+ }, [!bar.row.milestone && !bar.row.summary && bar.row.progress > 0 ? (openBlock(), createElementBlock("span", {
2677
+ key: 0,
2678
+ class: "dou-gantt__progress",
2679
+ style: normalizeStyle({ width: `${bar.row.progress * 100}%` })
2680
+ }, null, 4)) : createCommentVNode("", true), bar.label === "inside" ? (openBlock(), createElementBlock("span", _hoisted_23, [renderSlot(_ctx.$slots, "bar", {
2681
+ task: bar.row.task,
2682
+ row: bar.row
2683
+ }, () => [createTextVNode(toDisplayString(bar.row.task.title), 1)])])) : createCommentVNode("", true)], 46, _hoisted_22),
2684
+ editable.value && !bar.row.task.locked ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [!bar.row.milestone && !bar.row.summary ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
2685
+ createElementVNode("span", {
2686
+ class: "dou-gantt__handle is-start",
2687
+ "data-handle": "start",
2688
+ style: normalizeStyle({ left: `${bar.left}px` }),
2689
+ onPointerdown: ($event) => onBarDown($event, bar.row)
2690
+ }, null, 44, _hoisted_24),
2691
+ createElementVNode("span", {
2692
+ class: "dou-gantt__handle is-end",
2693
+ "data-handle": "end",
2694
+ style: normalizeStyle({ left: `${bar.left + bar.width}px` }),
2695
+ onPointerdown: ($event) => onBarDown($event, bar.row)
2696
+ }, null, 44, _hoisted_25),
2697
+ createElementVNode("span", {
2698
+ class: "dou-gantt__progress-handle",
2699
+ "data-handle": "progress",
2700
+ style: normalizeStyle({ left: `${bar.left + bar.width * bar.row.progress}px` }),
2701
+ onPointerdown: ($event) => onBarDown($event, bar.row)
2702
+ }, null, 44, _hoisted_26)
2703
+ ], 64)) : createCommentVNode("", true)], 64)) : createCommentVNode("", true),
2704
+ editable.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("span", {
2705
+ class: "dou-gantt__dot is-start",
2706
+ "data-handle": "link-start",
2707
+ style: normalizeStyle({ left: `${bar.left - (bar.row.milestone ? 7 : 0)}px` }),
2708
+ onPointerdown: ($event) => onBarDown($event, bar.row)
2709
+ }, null, 44, _hoisted_27), createElementVNode("span", {
2710
+ class: "dou-gantt__dot is-end",
2711
+ "data-handle": "link-end",
2712
+ style: normalizeStyle({ left: `${bar.left + (bar.row.milestone ? 7 : bar.width)}px` }),
2713
+ onPointerdown: ($event) => onBarDown($event, bar.row)
2714
+ }, null, 44, _hoisted_28)], 64)) : createCommentVNode("", true),
2715
+ bar.label === "outside" && !titled.value.has(bar.row.task.id) ? (openBlock(), createElementBlock("span", {
2716
+ key: 2,
2717
+ class: normalizeClass(["dou-gantt__bar-side", { "is-summary": bar.row.summary }]),
2718
+ style: normalizeStyle({ left: `${bar.left + sideOffset(bar)}px` })
2719
+ }, [renderSlot(_ctx.$slots, "bar", {
2720
+ task: bar.row.task,
2721
+ row: bar.row
2722
+ }, () => [createTextVNode(toDisplayString(bar.row.task.title), 1)])], 6)) : createCommentVNode("", true)
2723
+ ], 46, _hoisted_21);
2724
+ }), 128)),
2725
+ (openBlock(true), createElementBlock(Fragment, null, renderList(ghosts.value, (ghost) => {
2726
+ return openBlock(), createElementBlock("div", {
2727
+ key: ghost.id,
2728
+ class: normalizeClass(["dou-gantt__ghost", { "is-milestone": ghost.milestone }]),
2729
+ style: normalizeStyle({
2730
+ transform: `translate(${ghost.left}px, ${ghost.top}px)${ghost.milestone ? " rotate(45deg)" : ""}`,
2731
+ width: ghost.milestone ? void 0 : `${ghost.width}px`
2732
+ })
2733
+ }, null, 6);
2734
+ }), 128)),
2735
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.above, (layer, i) => {
2736
+ return openBlock(), createBlock(resolveDynamicComponent(layer), { key: `above-${i}` });
2737
+ }), 128)),
2738
+ dragTip.value ? (openBlock(), createElementBlock("div", {
2739
+ key: 2,
2740
+ class: normalizeClass(["dou-gantt__tip", { "is-refused": dragTip.value.refused }]),
2741
+ style: normalizeStyle({ transform: `translate(${dragTip.value.left}px, ${dragTip.value.top}px)` })
2742
+ }, toDisplayString(dragTip.value.text), 7)) : createCommentVNode("", true)
2743
+ ], 36)], 36)], 34),
2744
+ showTable.value ? (openBlock(), createElementBlock("div", {
2745
+ key: 2,
2746
+ class: "dou-gantt__divider",
2747
+ role: "separator",
2748
+ "aria-label": unref(messages).resizeTable,
2749
+ tabindex: "0",
2750
+ "aria-orientation": "vertical",
2751
+ "aria-valuenow": Math.round(tableSize.value),
2752
+ "aria-valuemin": TABLE_MIN,
2753
+ "aria-valuemax": tableMax.value,
2754
+ onPointerdown: onDividerDown,
2755
+ onKeydown: onDividerKey
2756
+ }, null, 40, _hoisted_29)) : createCommentVNode("", true),
2757
+ (openBlock(true), createElementBlock(Fragment, null, renderList(resolved.value.overlays, (overlay, i) => {
2758
+ return openBlock(), createBlock(resolveDynamicComponent(overlay), { key: `overlay-${i}` });
2759
+ }), 128)),
2760
+ createVNode(gantt_menu_default, {
2761
+ ref_key: "menu",
2762
+ ref: menu,
2763
+ open: menuOpen.value,
2764
+ "onUpdate:open": _cache[5] || (_cache[5] = ($event) => menuOpen.value = $event),
2765
+ items: menuItems.value
2766
+ }, null, 8, ["open", "items"]),
2767
+ renderSlot(_ctx.$slots, "default")
2768
+ ], 46, _hoisted_1);
2769
+ };
2770
+ }
2771
+ });
2772
+ export { gantt_vue_vue_type_script_setup_true_lang_default as default };
2773
+
2774
+ //# sourceMappingURL=gantt.vue_vue_type_script_setup_true_lang.js.map