@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,1359 @@
1
+ import gantt_menu_default from "../components/gantt-menu.js";
2
+ import time_grid_default from "../components/time-grid.js";
3
+ import { MINUTES_PER_DAY, parseTime } from "../core/time.js";
4
+ import time_header_default from "../components/time-header.js";
5
+ import { resolveCalendar } from "../core/calendar.js";
6
+ import { spanOf, writeSpan } from "../core/model.js";
7
+ import { snapStep } from "../core/scale.js";
8
+ import { format, useGanttMessages } from "../locale.js";
9
+ import { dateLabel, datesLabel, durationLabel, endLabel, markerRoom, spokenDuration } from "../labels.js";
10
+ import { GanttTransaction, emptyDocument } from "../core/transaction.js";
11
+ import { GanttHistory } from "../core/history.js";
12
+ import { readDocument } from "../core/document.js";
13
+ import { useTimeAxis } from "../time-axis.js";
14
+ import { groupByResource, loadSegments } from "./layout.js";
15
+ import { holdPointer, matchesHotkey, useElementSize, useLocale } from "dou-ui";
16
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toRaw, unref, useId, useModel, useTemplateRef, watch, withCtx } from "vue";
17
+ import { ChevronDownIcon, ChevronRightIcon, TrashIcon } from "@dou-ui/icons";
18
+ // src/scheduler/scheduler.vue?vue&type=script&setup=true&lang.ts
19
+ var _hoisted_1 = ["aria-label", "aria-describedby"];
20
+ var _hoisted_2 = ["id"];
21
+ var _hoisted_3 = {
22
+ class: "dou-gantt__sr-only",
23
+ "aria-live": "polite"
24
+ };
25
+ var _hoisted_4 = ["aria-label"];
26
+ var _hoisted_5 = { class: "dou-gantt__table-head" };
27
+ var _hoisted_6 = { class: "dou-gantt__th is-title" };
28
+ var _hoisted_7 = [
29
+ "data-row",
30
+ "aria-label",
31
+ "onPointerenter"
32
+ ];
33
+ var _hoisted_8 = { class: "dou-gantt__td is-title" };
34
+ var _hoisted_9 = [
35
+ "aria-label",
36
+ "aria-expanded",
37
+ "onClick"
38
+ ];
39
+ var _hoisted_10 = { class: "dou-gantt__cell-text" };
40
+ var _hoisted_11 = {
41
+ key: 0,
42
+ class: "dou-gantt__indent",
43
+ style: { "width": "16px" }
44
+ };
45
+ var _hoisted_12 = ["title"];
46
+ var _hoisted_13 = ["title"];
47
+ var _hoisted_14 = {
48
+ key: 0,
49
+ class: "dou-gantt__marker-text"
50
+ };
51
+ var _hoisted_15 = {
52
+ key: 0,
53
+ class: "dou-gantt__row-titles"
54
+ };
55
+ var _hoisted_16 = [
56
+ "data-task",
57
+ "aria-label",
58
+ "onPointerdown",
59
+ "onClick",
60
+ "onDblclick",
61
+ "onContextmenu"
62
+ ];
63
+ var _hoisted_17 = {
64
+ key: 0,
65
+ class: "dou-gantt__bar-label"
66
+ };
67
+ var _hoisted_18 = ["onPointerdown"];
68
+ var _hoisted_19 = ["onPointerdown"];
69
+ var _hoisted_20 = [
70
+ "aria-label",
71
+ "aria-valuenow",
72
+ "aria-valuemax"
73
+ ];
74
+ var HEADER = 56;
75
+ /** The distance from one lane to the next, in px. */
76
+ var LANE = 28;
77
+ /** Without the list, room above a resource's lanes for its name, in px. */
78
+ var CAPTION = 18;
79
+ var OVERSCAN = 240;
80
+ /** Below this width the list steps aside and the timeline takes the whole width. */
81
+ var NARROW = 520;
82
+ var TABLE_MIN = 120;
83
+ var LONG_PRESS = 300;
84
+ var EDGE = 32;
85
+ /** What the current drag would do. */
86
+ var scheduler_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
87
+ name: "DouScheduler",
88
+ __name: "scheduler",
89
+ props: /*@__PURE__*/ mergeModels({
90
+ resources: {},
91
+ calendar: {},
92
+ range: {},
93
+ today: {
94
+ type: Boolean,
95
+ default: true
96
+ },
97
+ weekStart: {},
98
+ markers: {},
99
+ table: {
100
+ type: Boolean,
101
+ default: true
102
+ },
103
+ tableWidth: { default: 200 },
104
+ rowHeight: { default: 36 },
105
+ load: {
106
+ type: Boolean,
107
+ default: true
108
+ },
109
+ readonly: { type: Boolean },
110
+ canMove: {},
111
+ menu: {
112
+ type: Boolean,
113
+ default: true
114
+ },
115
+ contextMenu: {},
116
+ ariaLabel: {},
117
+ messages: {}
118
+ }, {
119
+ "modelValue": { default: emptyDocument },
120
+ "modelModifiers": {},
121
+ "selection": { default: () => [] },
122
+ "selectionModifiers": {},
123
+ "scale": {},
124
+ "scaleModifiers": {}
125
+ }),
126
+ emits: /*@__PURE__*/ mergeModels([
127
+ "transaction",
128
+ "taskClick",
129
+ "taskDblclick",
130
+ "taskContextmenu",
131
+ "create"
132
+ ], [
133
+ "update:modelValue",
134
+ "update:selection",
135
+ "update:scale"
136
+ ]),
137
+ setup(__props, { expose: __expose, emit: __emit }) {
138
+ const props = __props;
139
+ const model = useModel(__props, "modelValue");
140
+ /** The `v-model`, with a wrongly shaped one read as well as it can be. */
141
+ const source = computed(() => readDocument(model.value, false));
142
+ const tasks = computed(() => source.value.tasks);
143
+ const selection = useModel(__props, "selection");
144
+ const scale = useModel(__props, "scale");
145
+ const emit = __emit;
146
+ const messages = useGanttMessages(() => props.messages);
147
+ const sm = computed(() => messages.value.scheduler);
148
+ const { locale } = useLocale();
149
+ const weekStart = computed(() => props.weekStart ?? locale.value.region.weekStart);
150
+ const calendar = computed(() => resolveCalendar(props.calendar));
151
+ const editable = computed(() => !props.readonly);
152
+ /** Height of the two-row time header, in px. */
153
+ const root = useTemplateRef("root");
154
+ const chart = useTemplateRef("chart");
155
+ const tableBody = useTemplateRef("tableBody");
156
+ const { width: rootWidth } = useElementSize(root);
157
+ const { width: viewWidth, height: viewHeight } = useElementSize(chart);
158
+ const narrow = computed(() => rootWidth.value > 0 && rootWidth.value < NARROW);
159
+ const showTable = computed(() => props.table && !narrow.value);
160
+ const caption = computed(() => showTable.value ? 0 : CAPTION);
161
+ const tableSize = ref(props.tableWidth);
162
+ watch(() => props.tableWidth, (width) => tableSize.value = width);
163
+ const history = new GanttHistory();
164
+ const canUndo = ref(false);
165
+ const canRedo = ref(false);
166
+ let latest = null;
167
+ const docOf = () => latest ?? toRaw(source.value);
168
+ function commit(tr) {
169
+ if (!latest) nextTick(() => latest = null);
170
+ latest = tr.doc;
171
+ model.value = tr.doc;
172
+ const ids = new Set(tr.doc.tasks.map((task) => task.id));
173
+ const kept = selection.value.filter((id) => ids.has(id));
174
+ if (kept.length !== selection.value.length) selection.value = kept;
175
+ emit("transaction", tr);
176
+ }
177
+ /** Applies changes as one transaction, one undo step. */
178
+ function transact(fn, meta) {
179
+ const tr = new GanttTransaction(docOf());
180
+ if (meta) tr.setMeta(meta);
181
+ fn(tr);
182
+ if (!tr.changed) return tr;
183
+ const before = selection.value;
184
+ commit(tr);
185
+ history.record(tr, before, selection.value);
186
+ canUndo.value = history.canUndo;
187
+ canRedo.value = history.canRedo;
188
+ const added = tr.steps.flatMap((step) => step.type === "addTask" ? [step.task.title] : []);
189
+ if (added.length) announce(format(messages.value.taskAdded, { title: added.join(messages.value.listSeparator) }));
190
+ return tr;
191
+ }
192
+ function travel(direction) {
193
+ const result = history[direction](docOf());
194
+ if (!result) return null;
195
+ result.tr.setMeta({
196
+ origin: direction,
197
+ history: false
198
+ });
199
+ commit(result.tr);
200
+ selection.value = result.selection;
201
+ canUndo.value = history.canUndo;
202
+ canRedo.value = history.canRedo;
203
+ return result.tr;
204
+ }
205
+ const undo = () => !!travel("undo");
206
+ const redo = () => !!travel("redo");
207
+ /** Undoes or redoes, and reads out the task it changed. */
208
+ function announceTravel(direction) {
209
+ const tr = travel(direction);
210
+ if (!tr) return;
211
+ const m = messages.value;
212
+ const step = tr.steps[direction === "undo" ? tr.steps.length - 1 : 0];
213
+ const id = step.type === "addTask" ? step.task.id : "id" in step ? step.id : "";
214
+ nextTick(() => {
215
+ const found = itemById.value.get(id);
216
+ const change = found ? describe(found.item, found.row) : format(m.taskRemoved, { title: tr.before.tasks.find((task) => task.id === id)?.title ?? "" });
217
+ announce(format(m[direction === "undo" ? "undone" : "redone"], { change }));
218
+ });
219
+ }
220
+ const collapsed = ref(/* @__PURE__ */ new Set());
221
+ const drag = shallowRef(null);
222
+ const taskList = computed(() => toRaw(tasks.value));
223
+ /** The tasks as shown: while dragging, as they would be after the drop. */
224
+ const shown = computed(() => {
225
+ if (!drag.value?.started) return taskList.value;
226
+ const plans = planned.value;
227
+ if (!plans?.length) return taskList.value;
228
+ const patches = new Map(plans.map((plan) => [plan.id, plan.patch]));
229
+ return taskList.value.map((task) => {
230
+ const patch = patches.get(task.id);
231
+ return patch ? {
232
+ ...task,
233
+ ...patch
234
+ } : task;
235
+ });
236
+ });
237
+ const placed = computed(() => groupByResource(shown.value, props.resources));
238
+ /**
239
+ * While dragging, rows keep the lanes they had: a row that shrank would
240
+ * pull the rows below up under the pointer.
241
+ */
242
+ const keptLanes = shallowRef(null);
243
+ const rows = computed(() => {
244
+ const out = [];
245
+ let top = 0;
246
+ const push = (row) => {
247
+ out.push({
248
+ ...row,
249
+ top
250
+ });
251
+ top += row.height;
252
+ };
253
+ const resourceRow = (resource) => {
254
+ const entry = placed.value.get(resource?.id ?? "");
255
+ const lanes = Math.max(entry?.lanes ?? 0, keptLanes.value?.get(resource?.id ?? "") ?? 0);
256
+ const load = loadSegments((entry?.items ?? []).map((item) => item.span));
257
+ const capacity = Math.max(1, resource?.capacity ?? 1);
258
+ return {
259
+ kind: "resource",
260
+ key: resource?.id ?? "",
261
+ resource,
262
+ title: resource?.title ?? sm.value.unassigned,
263
+ items: entry?.items ?? [],
264
+ lanes,
265
+ load,
266
+ capacity,
267
+ overloaded: !!resource && load.some((segment) => segment.count > capacity),
268
+ height: props.rowHeight + Math.max(0, lanes - 1) * LANE + caption.value
269
+ };
270
+ };
271
+ const done = /* @__PURE__ */ new Set();
272
+ for (const resource of props.resources) {
273
+ if (!resource.group) {
274
+ push(resourceRow(resource));
275
+ continue;
276
+ }
277
+ if (done.has(resource.group)) continue;
278
+ done.add(resource.group);
279
+ const key = `group:${resource.group}`;
280
+ push({
281
+ kind: "group",
282
+ key,
283
+ title: resource.group,
284
+ height: props.rowHeight
285
+ });
286
+ if (collapsed.value.has(key)) continue;
287
+ for (const member of props.resources) if (member.group === resource.group) push(resourceRow(member));
288
+ }
289
+ if (placed.value.has("")) push(resourceRow(null));
290
+ return out;
291
+ });
292
+ const rowByKey = computed(() => new Map(rows.value.map((row) => [row.key, row])));
293
+ const totalHeight = computed(() => {
294
+ const last = rows.value.at(-1);
295
+ return last ? last.top + last.height : 0;
296
+ });
297
+ /** Where each task is drawn. */
298
+ const itemById = computed(() => {
299
+ const out = /* @__PURE__ */ new Map();
300
+ for (const row of rows.value) if (row.kind === "resource") for (const item of row.items) out.set(item.task.id, {
301
+ item,
302
+ row
303
+ });
304
+ return out;
305
+ });
306
+ function toggle(row) {
307
+ const next = new Set(collapsed.value);
308
+ if (next.has(row.key)) next.delete(row.key);
309
+ else next.add(row.key);
310
+ collapsed.value = next;
311
+ }
312
+ const content = computed(() => {
313
+ let start = Infinity;
314
+ let end = -Infinity;
315
+ for (const entry of placed.value.values()) for (const { span } of entry.items) {
316
+ start = Math.min(start, span.start);
317
+ end = Math.max(end, span.end);
318
+ }
319
+ return start > end ? null : {
320
+ start,
321
+ end
322
+ };
323
+ });
324
+ const axis = useTimeAxis({
325
+ chart,
326
+ tableBody,
327
+ viewWidth,
328
+ scale,
329
+ range: () => props.range,
330
+ today: () => props.today,
331
+ weekStart,
332
+ calendar,
333
+ messages,
334
+ content,
335
+ fitWidth: () => showTable.value ? viewWidth.value : rootWidth.value
336
+ });
337
+ const { level, ppm, now, extent, origin, totalWidth, xOf, contentX, scrollTop, scrollLeft, scrollbar, topCells, bottomCells, shaded, todayX, timeWindow } = axis;
338
+ watch([viewHeight, viewWidth], axis.measureScrollbar, { flush: "post" });
339
+ watch([rootWidth, viewWidth], axis.fitWhenSized);
340
+ const markers = computed(() => {
341
+ const inRange = (props.markers ?? []).flatMap((marker, index) => {
342
+ const at = parseTime(marker.date)?.at;
343
+ if (at == null || at < extent.value.start || at > extent.value.end) return [];
344
+ return [{
345
+ key: `${index}:${marker.date}`,
346
+ x: xOf(at),
347
+ marker
348
+ }];
349
+ });
350
+ const room = markerRoom(inRange.map((item) => item.marker.label ? item.x : -Infinity));
351
+ return inRange.map((item, i) => ({
352
+ ...item,
353
+ room: room[i]
354
+ }));
355
+ });
356
+ const bodyHeight = computed(() => Math.max(0, viewHeight.value - HEADER));
357
+ /** The rows in view, with some room above and below. */
358
+ const visibleRows = computed(() => {
359
+ const list = rows.value;
360
+ const from = scrollTop.value - OVERSCAN;
361
+ const to = scrollTop.value + bodyHeight.value + OVERSCAN;
362
+ let lo = 0;
363
+ let hi = list.length;
364
+ while (lo < hi) {
365
+ const mid = lo + hi >> 1;
366
+ if (list[mid].top + list[mid].height <= from) lo = mid + 1;
367
+ else hi = mid;
368
+ }
369
+ const out = [];
370
+ for (let i = lo; i < list.length && list[i].top < to; i++) out.push(list[i]);
371
+ return out;
372
+ });
373
+ const resourceRows = computed(() => visibleRows.value.filter((row) => row.kind === "resource"));
374
+ /** Where a lane's bar row sits, in body px. */
375
+ const laneTop = (row, lane) => row.top + caption.value + lane * LANE;
376
+ const describeRow = (row) => {
377
+ const text = format(sm.value.row, {
378
+ title: row.title,
379
+ count: row.items.length
380
+ });
381
+ return row.overloaded ? format(sm.value.overloaded, { row: text }) : text;
382
+ };
383
+ function describe(item, row) {
384
+ const m = messages.value;
385
+ const { task, span } = item;
386
+ const text = task.type === "milestone" || span.end <= span.start ? format(m.milestone, {
387
+ title: task.title,
388
+ start: dateLabel(span.start - (span.dateOnly ? MINUTES_PER_DAY / 2 : 0), span.dateOnly, m)
389
+ }) : format(sm.value.task, {
390
+ title: task.title,
391
+ dates: datesLabel(span, m),
392
+ duration: spokenDuration(span, calendar.value, m)
393
+ });
394
+ return format(sm.value.item, {
395
+ task: text,
396
+ resource: row.title
397
+ });
398
+ }
399
+ const selected = computed(() => new Set(selection.value));
400
+ let canvas;
401
+ /** Width of a bar title in px; estimated where text can't be measured. */
402
+ function textWidth(text) {
403
+ if (canvas === void 0 && root.value) try {
404
+ canvas = document.createElement("canvas").getContext("2d");
405
+ if (canvas) {
406
+ const style = getComputedStyle(root.value);
407
+ canvas.font = `${style.getPropertyValue("--dou-font-size-sm").trim() || "13px"} ${style.fontFamily}`;
408
+ }
409
+ } catch {
410
+ canvas = null;
411
+ }
412
+ return canvas ? canvas.measureText(text).width : text.length * 13;
413
+ }
414
+ const bars = computed(() => {
415
+ const { start, end } = timeWindow.value;
416
+ const out = [];
417
+ for (const row of resourceRows.value) for (const item of row.items) {
418
+ const { span } = item;
419
+ if (span.end < start || span.start > end) continue;
420
+ const milestone = item.task.type === "milestone";
421
+ const width = Math.max(milestone ? 0 : 2, (span.end - span.start) * ppm.value);
422
+ out.push({
423
+ item,
424
+ row,
425
+ left: xOf(span.start),
426
+ width,
427
+ top: laneTop(row, item.lane),
428
+ milestone,
429
+ outside: milestone || textWidth(item.task.title) + 20 > width,
430
+ aria: describe(item, row)
431
+ });
432
+ }
433
+ return out;
434
+ });
435
+ /** How busy each row in view is, as stretches under it. */
436
+ const loads = computed(() => {
437
+ if (!props.load) return [];
438
+ const { start, end } = timeWindow.value;
439
+ return resourceRows.value.flatMap((row) => row.resource ? row.load.filter((segment) => segment.end > start && segment.start < end).map((segment) => ({
440
+ key: `${row.key}:${segment.start}`,
441
+ top: row.top + row.height - 6,
442
+ left: xOf(segment.start),
443
+ width: Math.max(1, (segment.end - segment.start) * ppm.value),
444
+ level: Math.min(1, segment.count / row.capacity),
445
+ over: segment.count > row.capacity
446
+ })) : []);
447
+ });
448
+ /** Without the list, rows show their title at the left edge of the timeline. */
449
+ const rowTitles = computed(() => showTable.value ? [] : visibleRows.value);
450
+ const tableMax = computed(() => Math.max(TABLE_MIN, Math.round((rootWidth.value || 1e3) * .5)));
451
+ const hovered = ref(null);
452
+ function onDividerDown(event) {
453
+ if (event.button !== 0) return;
454
+ event.preventDefault();
455
+ const x0 = event.clientX;
456
+ const width0 = tableSize.value;
457
+ const move = (e) => tableSize.value = Math.min(tableMax.value, Math.max(TABLE_MIN, width0 + e.clientX - x0));
458
+ const up = () => {
459
+ window.removeEventListener("pointermove", move);
460
+ window.removeEventListener("pointerup", up);
461
+ window.removeEventListener("pointercancel", up);
462
+ };
463
+ window.addEventListener("pointermove", move);
464
+ window.addEventListener("pointerup", up);
465
+ window.addEventListener("pointercancel", up);
466
+ }
467
+ function onDividerKey(event) {
468
+ const step = event.shiftKey ? 40 : 10;
469
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
470
+ event.preventDefault();
471
+ const next = tableSize.value + (event.key === "ArrowLeft" ? -step : step);
472
+ tableSize.value = Math.min(tableMax.value, Math.max(TABLE_MIN, next));
473
+ }
474
+ const active = ref(null);
475
+ function select(task, event) {
476
+ const id = task.id;
477
+ if (event.shiftKey || event.metaKey || event.ctrlKey) selection.value = selected.value.has(id) ? selection.value.filter((item) => item !== id) : [...selection.value, id];
478
+ else if (selection.value.length !== 1 || selection.value[0] !== id) selection.value = [id];
479
+ }
480
+ function onTaskClick(task, event) {
481
+ if (dragged) return;
482
+ active.value = task.id;
483
+ select(task, event);
484
+ emit("taskClick", task, event);
485
+ }
486
+ const THRESHOLD = {
487
+ mouse: 3,
488
+ touch: 6
489
+ };
490
+ let dragged = false;
491
+ let pressTimer;
492
+ let frame = 0;
493
+ let hold = null;
494
+ const snap = (at, step) => Math.round(at / step) * step;
495
+ const edgeSpeed = (gap) => gap < EDGE ? Math.ceil((EDGE - gap) / EDGE * 16) : 0;
496
+ /** The step a drag snaps to: whole days when any task is date-only. */
497
+ function stepFor(spans) {
498
+ const unit = snapStep(axis.dayWidth.value);
499
+ for (const span of spans) if (span.dateOnly) return Math.max(unit, MINUTES_PER_DAY);
500
+ return unit;
501
+ }
502
+ /** The body y of a pointer. */
503
+ const contentY = (clientY) => clientY - chart.value.getBoundingClientRect().top - HEADER + chart.value.scrollTop;
504
+ /** The resource row at a body y, if any. */
505
+ function resourceRowAt(y) {
506
+ const list = rows.value;
507
+ let lo = 0;
508
+ let hi = list.length - 1;
509
+ while (lo <= hi) {
510
+ const mid = lo + hi >> 1;
511
+ const row = list[mid];
512
+ if (y < row.top) hi = mid - 1;
513
+ else if (y >= row.top + row.height) lo = mid + 1;
514
+ else return row.kind === "resource" ? row : null;
515
+ }
516
+ return null;
517
+ }
518
+ const taskById = (id) => docOf().tasks.find((task) => task.id === id);
519
+ const planned = computed(() => {
520
+ const d = drag.value;
521
+ if (!d?.started || d.mode === "draw") return null;
522
+ const delta = (d.x - d.x0) / ppm.value;
523
+ const out = [];
524
+ const first = d.spans.get(d.id);
525
+ const unit = stepFor([...d.spans.values()].map((entry) => entry.span));
526
+ for (const [id, entry] of d.spans) {
527
+ const task = taskById(id);
528
+ if (!task) continue;
529
+ const { span } = entry;
530
+ let start = span.start;
531
+ let end = span.end;
532
+ if (d.mode === "move") {
533
+ const shift = snap(first.span.start + delta, unit) - first.span.start;
534
+ start += shift;
535
+ end += shift;
536
+ } else if (d.mode === "start") start = Math.min(snap(span.start + delta, unit), span.end - unit);
537
+ else end = Math.max(snap(span.end + delta, unit), span.start + unit);
538
+ const row = d.mode === "move" && entry.row === d.from ? d.to : entry.row;
539
+ const patch = writeSpan(task, start, end);
540
+ if (row !== entry.row) patch.resource = row || void 0;
541
+ out.push({
542
+ id,
543
+ span: {
544
+ start,
545
+ end,
546
+ dateOnly: span.dateOnly
547
+ },
548
+ row,
549
+ patch
550
+ });
551
+ }
552
+ return out;
553
+ });
554
+ /**
555
+ * Why `canMove` refuses changes, or `null` when it allows them all. Each
556
+ * patch is checked with the resource it ends up with.
557
+ */
558
+ function checkMove(patches) {
559
+ const rule = props.canMove;
560
+ if (!rule) return null;
561
+ const doc = docOf();
562
+ for (const { id, patch } of patches) {
563
+ const task = taskById(id);
564
+ if (!task) continue;
565
+ const next = {
566
+ ...task,
567
+ ...patch
568
+ };
569
+ if (!next.start) continue;
570
+ const known = next.resource && props.resources.some((item) => item.id === next.resource);
571
+ const answer = rule({
572
+ task,
573
+ start: next.start,
574
+ end: next.end,
575
+ resource: known ? next.resource : void 0
576
+ }, doc);
577
+ if (answer === true) continue;
578
+ return typeof answer === "string" && answer ? answer : messages.value.moveRefused;
579
+ }
580
+ return null;
581
+ }
582
+ /** Why the current drag would be refused. */
583
+ const dragRefusal = computed(() => {
584
+ const plans = planned.value;
585
+ return plans?.length ? checkMove(plans) : null;
586
+ });
587
+ /** The range being drawn on an empty part of a row. */
588
+ const drawn = computed(() => {
589
+ const d = drag.value;
590
+ if (!d?.started || d.mode !== "draw") return null;
591
+ const unit = snapStep(axis.dayWidth.value);
592
+ const a = origin.value + Math.min(d.x0, d.x) / ppm.value;
593
+ const b = origin.value + Math.max(d.x0, d.x) / ppm.value;
594
+ const start = Math.floor(a / unit) * unit;
595
+ const end = Math.max(start + unit, Math.ceil(b / unit) * unit);
596
+ return {
597
+ row: d.from,
598
+ start,
599
+ end,
600
+ dateOnly: unit >= MINUTES_PER_DAY
601
+ };
602
+ });
603
+ /** A dashed outline over the range being drawn. */
604
+ const drawnBox = computed(() => {
605
+ const range = drawn.value;
606
+ const row = range && rowByKey.value.get(range.row);
607
+ if (!range || row?.kind !== "resource") return null;
608
+ return {
609
+ left: xOf(range.start),
610
+ width: (range.end - range.start) * ppm.value,
611
+ top: laneTop(row, 0)
612
+ };
613
+ });
614
+ /** The new dates beside the pointer while dragging. */
615
+ const dragTip = computed(() => {
616
+ const d = drag.value;
617
+ if (!d?.started) return null;
618
+ const m = messages.value;
619
+ const found = d.mode === "draw" ? void 0 : itemById.value.get(d.id);
620
+ const span = found?.item.span ?? (drawn.value && { ...drawn.value });
621
+ const row = found?.row ?? rowByKey.value.get(d.from);
622
+ if (!span || row?.kind !== "resource") return null;
623
+ const refusal = d.mode === "draw" ? null : dragRefusal.value;
624
+ return {
625
+ text: refusal ? refusal : span.end > span.start ? format(m.rangeTip, {
626
+ start: dateLabel(span.start, span.dateOnly, m),
627
+ end: endLabel(span, m),
628
+ duration: durationLabel(span, calendar.value, m)
629
+ }) : dateLabel(span.start - (span.dateOnly ? MINUTES_PER_DAY / 2 : 0), span.dateOnly, m),
630
+ refused: !!refusal,
631
+ left: Math.max(scrollLeft.value + 8, xOf(span.start)),
632
+ top: laneTop(row, found?.item.lane ?? 0)
633
+ };
634
+ });
635
+ function startDrag(event, mode, id, from) {
636
+ const touch = event.pointerType === "touch";
637
+ const spans = /* @__PURE__ */ new Map();
638
+ if (mode !== "draw") {
639
+ const ids = mode === "move" && selected.value.has(id) ? selection.value : [id];
640
+ for (const each of ids) {
641
+ const found = itemById.value.get(each);
642
+ if (found && !found.item.task.locked) spans.set(each, {
643
+ span: found.item.span,
644
+ row: found.row.key,
645
+ lane: found.item.lane
646
+ });
647
+ }
648
+ if (!spans.has(id)) return;
649
+ }
650
+ hold?.release();
651
+ hold = holdPointer(event, root.value);
652
+ const x = contentX(event.clientX);
653
+ const y = contentY(event.clientY);
654
+ const d = {
655
+ mode,
656
+ id,
657
+ pointerId: event.pointerId,
658
+ touch,
659
+ x0: x,
660
+ x,
661
+ y0: y,
662
+ y,
663
+ clientX: event.clientX,
664
+ clientY: event.clientY,
665
+ started: false,
666
+ spans,
667
+ from: from.key,
668
+ to: from.key
669
+ };
670
+ drag.value = d;
671
+ window.addEventListener("pointermove", onDragMove);
672
+ window.addEventListener("pointerup", onDragEnd);
673
+ window.addEventListener("pointercancel", stopDrag);
674
+ window.addEventListener("keydown", onDragKey, true);
675
+ if (touch) pressTimer = setTimeout(() => {
676
+ if (drag.value !== d) return;
677
+ begin(d);
678
+ navigator.vibrate?.(8);
679
+ }, LONG_PRESS);
680
+ }
681
+ function begin(d) {
682
+ d.started = true;
683
+ keptLanes.value = new Map(rows.value.flatMap((row) => row.kind === "resource" ? [[row.key, row.lanes]] : []));
684
+ hold?.grab();
685
+ window.getSelection()?.removeAllRanges();
686
+ frame = requestAnimationFrame(autoScroll);
687
+ drag.value = { ...d };
688
+ }
689
+ function update(d, x, y) {
690
+ const next = {
691
+ ...d,
692
+ x,
693
+ y
694
+ };
695
+ if (d.mode === "move") next.to = resourceRowAt(y)?.key ?? d.to;
696
+ drag.value = next;
697
+ }
698
+ function onDragMove(event) {
699
+ const d = drag.value;
700
+ if (!d || event.pointerId !== d.pointerId) return;
701
+ d.clientX = event.clientX;
702
+ d.clientY = event.clientY;
703
+ const x = contentX(event.clientX);
704
+ const y = contentY(event.clientY);
705
+ if (!d.started) {
706
+ const moved = Math.hypot(x - d.x0, y - d.y0);
707
+ if (d.touch) {
708
+ if (moved > THRESHOLD.touch) stopDrag();
709
+ return;
710
+ }
711
+ if (moved < THRESHOLD.mouse) return;
712
+ begin(d);
713
+ }
714
+ update(drag.value, x, y);
715
+ }
716
+ function autoScroll() {
717
+ const d = drag.value;
718
+ const el = chart.value;
719
+ if (!d?.started || !el) return;
720
+ const rect = el.getBoundingClientRect();
721
+ let dx = -edgeSpeed(d.clientX - rect.left) || edgeSpeed(rect.right - d.clientX);
722
+ if (Math.sign(dx) !== Math.sign(d.x - d.x0)) dx = 0;
723
+ let dy = -edgeSpeed(d.clientY - rect.top - HEADER) || edgeSpeed(rect.bottom - d.clientY);
724
+ if (d.mode !== "move" || Math.sign(dy) !== Math.sign(d.y - d.y0)) dy = 0;
725
+ if (dx || dy) {
726
+ el.scrollLeft += dx;
727
+ el.scrollTop += dy;
728
+ update(d, contentX(d.clientX), contentY(d.clientY));
729
+ }
730
+ frame = requestAnimationFrame(autoScroll);
731
+ }
732
+ function onDragEnd(event) {
733
+ const d = drag.value;
734
+ if (!d || event.pointerId !== d.pointerId) return;
735
+ if (d.touch && d.started && d.mode === "move" && Math.abs(d.x - d.x0) < THRESHOLD.touch) {
736
+ const found = itemById.value.get(d.id);
737
+ stopDrag();
738
+ if (found && menuAllowed(found.item.task, event.clientX, event.clientY)) showMenu(taskTarget(found.item.task, found.row), event.clientX, event.clientY);
739
+ return;
740
+ }
741
+ if (d.started) {
742
+ dragged = true;
743
+ setTimeout(() => dragged = false);
744
+ if (d.mode === "draw") finishDraw();
745
+ else finishMove(d);
746
+ } else if (d.mode === "draw" && !d.touch) selection.value = [];
747
+ stopDrag();
748
+ }
749
+ function finishMove(d) {
750
+ const plans = planned.value;
751
+ if (!plans?.length) return;
752
+ if (dragRefusal.value) return void announce(dragRefusal.value);
753
+ transact((tr) => {
754
+ for (const plan of plans) tr.updateTask(plan.id, plan.patch);
755
+ });
756
+ const pressed = plans.find((plan) => plan.id === d.id);
757
+ const row = pressed && rowByKey.value.get(pressed.row);
758
+ if (pressed && pressed.row !== d.from && row) announce(format(sm.value.moved, {
759
+ title: taskById(d.id)?.title ?? "",
760
+ resource: row.title
761
+ }));
762
+ }
763
+ function finishDraw() {
764
+ const range = drawn.value;
765
+ if (!range) return;
766
+ const span = writeSpan({
767
+ id: "",
768
+ title: ""
769
+ }, range.start, range.end);
770
+ const out = {
771
+ resource: range.row || void 0,
772
+ start: span.start,
773
+ end: span.end ?? span.start
774
+ };
775
+ emit("create", out);
776
+ }
777
+ function onDragKey(event) {
778
+ if (event.key !== "Escape") return;
779
+ event.preventDefault();
780
+ event.stopPropagation();
781
+ stopDrag();
782
+ }
783
+ function stopDrag() {
784
+ clearTimeout(pressTimer);
785
+ cancelAnimationFrame(frame);
786
+ hold?.release();
787
+ hold = null;
788
+ drag.value = null;
789
+ keptLanes.value = null;
790
+ window.removeEventListener("pointermove", onDragMove);
791
+ window.removeEventListener("pointerup", onDragEnd);
792
+ window.removeEventListener("pointercancel", stopDrag);
793
+ window.removeEventListener("keydown", onDragKey, true);
794
+ }
795
+ function onBarDown(event, bar) {
796
+ if (event.button !== 0 || !editable.value) return;
797
+ event.stopPropagation();
798
+ const handle = event.currentTarget.dataset.handle;
799
+ const mode = handle === "start" || handle === "end" ? handle : "move";
800
+ if (bar.item.task.locked) return;
801
+ startDrag(event, mode, bar.item.task.id, bar.row);
802
+ }
803
+ function onBodyDown(event) {
804
+ if (event.button !== 0 || !editable.value) return;
805
+ const row = resourceRowAt(contentY(event.clientY));
806
+ if (row) startDrag(event, "draw", "", row);
807
+ else if (event.pointerType !== "touch") selection.value = [];
808
+ }
809
+ const onTouchMove = (event) => void axis.pinchMove(event);
810
+ const onTouchEnd = (event) => axis.pinchEnd(event);
811
+ function onTouchStart(event) {
812
+ if (axis.pinchStart(event)) stopDrag();
813
+ }
814
+ onMounted(() => {
815
+ const el = root.value;
816
+ el.addEventListener("touchstart", onTouchStart, { passive: true });
817
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
818
+ el.addEventListener("touchend", onTouchEnd);
819
+ el.addEventListener("touchcancel", onTouchEnd);
820
+ });
821
+ onBeforeUnmount(() => {
822
+ const el = root.value;
823
+ el?.removeEventListener("touchstart", onTouchStart);
824
+ el?.removeEventListener("touchmove", onTouchMove);
825
+ el?.removeEventListener("touchend", onTouchEnd);
826
+ el?.removeEventListener("touchcancel", onTouchEnd);
827
+ stopDrag();
828
+ });
829
+ const menu = useTemplateRef("menu");
830
+ const menuItems = shallowRef([]);
831
+ const menuOpen = ref(false);
832
+ /** The moment under a point on screen, or the task's start when it is off the timeline. */
833
+ function timeAt(x, fallback) {
834
+ const rect = chart.value.getBoundingClientRect();
835
+ if (x < rect.left || x > rect.right) return fallback;
836
+ return Math.round(origin.value + contentX(x) / ppm.value);
837
+ }
838
+ const taskTarget = (task, row) => ({
839
+ kind: "task",
840
+ task,
841
+ resource: row.resource,
842
+ time: 0
843
+ });
844
+ /** Your items, then the built-in one, a separator between them. */
845
+ function itemsFor(target) {
846
+ const items = [...props.contextMenu?.(target) ?? []];
847
+ if (props.menu && editable.value && target.kind === "task") {
848
+ const ids = selected.value.has(target.task.id) ? selection.value : [target.task.id];
849
+ const m = messages.value.menu;
850
+ items.push({
851
+ key: "remove",
852
+ label: ids.length > 1 ? format(m.removeTasks, { count: ids.length }) : m.removeTask,
853
+ icon: TrashIcon,
854
+ shortcut: "delete",
855
+ tone: "danger",
856
+ divided: items.length > 0,
857
+ run: () => removeTasks(ids)
858
+ });
859
+ }
860
+ return items;
861
+ }
862
+ /** Opens the menu at a point on screen; `false` when it has nothing to show. */
863
+ function showMenu(on, x, y) {
864
+ const start = on.kind === "task" ? itemById.value.get(on.task.id)?.item.span.start ?? 0 : 0;
865
+ const target = {
866
+ ...on,
867
+ time: timeAt(x, start)
868
+ };
869
+ const items = itemsFor(target);
870
+ if (!items.length) return false;
871
+ if (target.kind === "task") {
872
+ active.value = target.task.id;
873
+ if (!selected.value.has(target.task.id)) selection.value = [target.task.id];
874
+ }
875
+ menuItems.value = items;
876
+ menu.value?.open(x, y);
877
+ return true;
878
+ }
879
+ /** When a finger last went down: its long press opens the menu itself, not the browser's. */
880
+ let touchedAt = 0;
881
+ const onRootPointerdown = (event) => {
882
+ if (event.pointerType === "touch") touchedAt = Date.now();
883
+ };
884
+ /**
885
+ * A menu opened by a resting finger or by the menu key has no browser event:
886
+ * it gets a stand-in, so `taskContextmenu` comes first in every case and
887
+ * `preventDefault()` keeps the menu closed.
888
+ */
889
+ function menuAllowed(item, x, y) {
890
+ const event = new MouseEvent("contextmenu", {
891
+ cancelable: true,
892
+ clientX: x,
893
+ clientY: y
894
+ });
895
+ emit("taskContextmenu", item, event);
896
+ return !event.defaultPrevented;
897
+ }
898
+ function onTaskContextmenu(bar, event) {
899
+ if (Date.now() - touchedAt < 1e3) return event.preventDefault();
900
+ emit("taskContextmenu", bar.item.task, event);
901
+ if (event.defaultPrevented) return;
902
+ if (showMenu(taskTarget(bar.item.task, bar.row), event.clientX, event.clientY)) event.preventDefault();
903
+ }
904
+ function onBodyContextmenu(event) {
905
+ const target = event.target;
906
+ if (event.defaultPrevented || target.closest(".dou-gantt__bar-row")) return;
907
+ if (Date.now() - touchedAt < 1e3) return event.preventDefault();
908
+ const row = resourceRowAt(contentY(event.clientY));
909
+ if (!row) return;
910
+ if (showMenu({
911
+ kind: "row",
912
+ resource: row.resource,
913
+ time: 0
914
+ }, event.clientX, event.clientY)) event.preventDefault();
915
+ }
916
+ /** The menu key, or Shift+F10, opens the menu of the task the keyboard is on, at its bar. */
917
+ function openKeyMenu() {
918
+ const found = active.value ? itemById.value.get(active.value) : void 0;
919
+ if (!found) return false;
920
+ const rect = (root.value?.querySelector(`.dou-gantt__bar[data-task="${CSS.escape(found.item.task.id)}"]`))?.getBoundingClientRect();
921
+ if (!rect) return false;
922
+ if (!menuAllowed(found.item.task, rect.left + 8, rect.bottom + 4)) return true;
923
+ return showMenu(taskTarget(found.item.task, found.row), rect.left + 8, rect.bottom + 4);
924
+ }
925
+ const keysHint = useId();
926
+ const live = ref("");
927
+ function announce(text) {
928
+ live.value = "";
929
+ nextTick(() => live.value = text);
930
+ }
931
+ /** The tasks in reading order: row by row, by start. */
932
+ const ordered = computed(() => {
933
+ const out = [];
934
+ const list = rows.value.filter((row) => row.kind === "resource");
935
+ list.forEach((row, index) => {
936
+ const items = [...row.items].sort((a, b) => a.span.start - b.span.start || a.lane - b.lane);
937
+ for (const item of items) out.push({
938
+ item,
939
+ row,
940
+ index
941
+ });
942
+ });
943
+ return {
944
+ out,
945
+ rows: list
946
+ };
947
+ });
948
+ function focusTask(id) {
949
+ active.value = id;
950
+ scrollToTask(id);
951
+ nextTick(() => {
952
+ (root.value?.querySelector(`.dou-gantt__bar[data-task="${CSS.escape(id)}"]`) ?? root.value)?.focus({ preventScroll: true });
953
+ });
954
+ }
955
+ function scrollToTask(id) {
956
+ const found = itemById.value.get(id);
957
+ const el = chart.value;
958
+ if (!found || !el) return;
959
+ const top = laneTop(found.row, found.item.lane);
960
+ if (top < el.scrollTop) el.scrollTop = top;
961
+ else if (top + props.rowHeight > el.scrollTop + bodyHeight.value) el.scrollTop = top + props.rowHeight - bodyHeight.value;
962
+ const left = xOf(found.item.span.start);
963
+ const right = xOf(found.item.span.end);
964
+ if (left < scrollLeft.value || right > scrollLeft.value + viewWidth.value) axis.scrollToTime(found.item.span.start);
965
+ }
966
+ /** Arrow keys: the next task along, or the one nearest in time in the row above or below. */
967
+ function stepFocus(key) {
968
+ const { out, rows: list } = ordered.value;
969
+ if (!out.length) return;
970
+ const at = out.findIndex((entry) => entry.item.task.id === active.value);
971
+ if (at === -1) return void focusTask(out[0].item.task.id);
972
+ const current = out[at];
973
+ if (key === "ArrowLeft" || key === "ArrowRight") {
974
+ const next = out[at + (key === "ArrowLeft" ? -1 : 1)];
975
+ if (next && next.row === current.row) focusTask(next.item.task.id);
976
+ return;
977
+ }
978
+ const direction = key === "ArrowUp" ? -1 : 1;
979
+ for (let i = current.index + direction; i >= 0 && i < list.length; i += direction) {
980
+ const candidates = out.filter((entry) => entry.index === i);
981
+ if (!candidates.length) continue;
982
+ const time = current.item.span.start;
983
+ let best = candidates[0];
984
+ for (const entry of candidates) if (Math.abs(entry.item.span.start - time) < Math.abs(best.item.span.start - time)) best = entry;
985
+ focusTask(best.item.task.id);
986
+ return;
987
+ }
988
+ }
989
+ /** Alt and the arrow keys: earlier or later, or to the resource above or below. */
990
+ function keyMove(key) {
991
+ const found = active.value ? itemById.value.get(active.value) : void 0;
992
+ if (!found || found.item.task.locked) return;
993
+ const { item, row } = found;
994
+ const task = item.task;
995
+ if (key === "ArrowLeft" || key === "ArrowRight") {
996
+ const unit = stepFor([item.span]);
997
+ const shift = key === "ArrowLeft" ? -unit : unit;
998
+ const patch = writeSpan(task, item.span.start + shift, item.span.end + shift);
999
+ const refusal = checkMove([{
1000
+ id: task.id,
1001
+ patch
1002
+ }]);
1003
+ if (refusal) return void announce(refusal);
1004
+ transact((tr) => tr.updateTask(task.id, patch), { group: `key-move:${task.id}` });
1005
+ const span = spanOf(taskById(task.id));
1006
+ if (span) announce(describe({
1007
+ ...item,
1008
+ span
1009
+ }, row));
1010
+ return;
1011
+ }
1012
+ const list = ordered.value.rows;
1013
+ const target = list[list.indexOf(row) + (key === "ArrowUp" ? -1 : 1)];
1014
+ if (!target) return;
1015
+ const refusal = checkMove([{
1016
+ id: task.id,
1017
+ patch: { resource: target.resource?.id }
1018
+ }]);
1019
+ if (refusal) return void announce(refusal);
1020
+ transact((tr) => tr.updateTask(task.id, { resource: target.resource?.id }));
1021
+ announce(format(sm.value.moved, {
1022
+ title: task.title,
1023
+ resource: target.title
1024
+ }));
1025
+ focusTask(task.id);
1026
+ }
1027
+ function removeSelected() {
1028
+ const ids = selection.value.length ? selection.value : active.value ? [active.value] : [];
1029
+ if (ids.length) removeTasks(ids);
1030
+ }
1031
+ function removeTasks(ids) {
1032
+ const titles = ids.map((id) => taskById(id)?.title).filter(Boolean);
1033
+ transact((tr) => {
1034
+ for (const id of ids) tr.removeTask(id);
1035
+ });
1036
+ active.value = null;
1037
+ const m = messages.value;
1038
+ announce(format(m.tasksRemoved, { titles: titles.join(m.listSeparator) }));
1039
+ }
1040
+ function onKeydown(event) {
1041
+ if (drag.value) return;
1042
+ const key = event.key;
1043
+ if (matchesHotkey(event, "mod+z")) {
1044
+ event.preventDefault();
1045
+ if (editable.value) announceTravel("undo");
1046
+ return;
1047
+ }
1048
+ if (matchesHotkey(event, "mod+shift+z") || matchesHotkey(event, "mod+y")) {
1049
+ event.preventDefault();
1050
+ if (editable.value) announceTravel("redo");
1051
+ return;
1052
+ }
1053
+ if (key.startsWith("Arrow")) {
1054
+ event.preventDefault();
1055
+ if (event.altKey) {
1056
+ if (editable.value) keyMove(key);
1057
+ } else stepFocus(key);
1058
+ return;
1059
+ }
1060
+ if (key === "ContextMenu" || matchesHotkey(event, "shift+f10")) {
1061
+ if (openKeyMenu()) event.preventDefault();
1062
+ return;
1063
+ }
1064
+ if ((key === "Delete" || key === "Backspace") && editable.value) {
1065
+ event.preventDefault();
1066
+ removeSelected();
1067
+ return;
1068
+ }
1069
+ if ((key === "Enter" || key === " ") && active.value) {
1070
+ event.preventDefault();
1071
+ const task = taskById(active.value);
1072
+ if (task) select(task, event);
1073
+ return;
1074
+ }
1075
+ if (key === "Escape" && selection.value.length) {
1076
+ event.preventDefault();
1077
+ selection.value = [];
1078
+ }
1079
+ }
1080
+ __expose({
1081
+ transact,
1082
+ undo,
1083
+ redo,
1084
+ get canUndo() {
1085
+ return canUndo.value;
1086
+ },
1087
+ get canRedo() {
1088
+ return canRedo.value;
1089
+ },
1090
+ fit: axis.fit,
1091
+ scrollToDate: axis.scrollToDate,
1092
+ scrollToToday: axis.scrollToToday,
1093
+ scrollToTask,
1094
+ focusTask,
1095
+ zoomIn: axis.zoomIn,
1096
+ zoomOut: axis.zoomOut
1097
+ });
1098
+ return (_ctx, _cache) => {
1099
+ return openBlock(), createElementBlock("div", {
1100
+ ref_key: "root",
1101
+ ref: root,
1102
+ class: normalizeClass(["dou-gantt is-scheduler", {
1103
+ "is-narrow": narrow.value,
1104
+ "is-editable": editable.value,
1105
+ "is-dragging": drag.value?.started,
1106
+ [`is-dragging-${drag.value?.mode}`]: drag.value?.started
1107
+ }]),
1108
+ style: normalizeStyle({
1109
+ "--dou-gantt-row-height": `${__props.rowHeight}px`,
1110
+ "--dou-gantt-lane": `${LANE}px`
1111
+ }),
1112
+ role: "group",
1113
+ tabindex: "0",
1114
+ "aria-label": __props.ariaLabel ?? sm.value.name,
1115
+ "aria-describedby": unref(keysHint),
1116
+ onKeydown,
1117
+ onPointerdownCapture: onRootPointerdown
1118
+ }, [
1119
+ createElementVNode("span", {
1120
+ id: unref(keysHint),
1121
+ class: "dou-gantt__sr-only"
1122
+ }, toDisplayString(sm.value.keys), 9, _hoisted_2),
1123
+ createElementVNode("div", _hoisted_3, toDisplayString(live.value), 1),
1124
+ showTable.value ? (openBlock(), createElementBlock("div", {
1125
+ key: 0,
1126
+ class: "dou-gantt__table-pane",
1127
+ style: normalizeStyle({ width: `${tableSize.value}px` })
1128
+ }, [createElementVNode("div", {
1129
+ class: "dou-gantt__table",
1130
+ role: "list",
1131
+ "aria-label": sm.value.resource
1132
+ }, [createElementVNode("div", _hoisted_5, [createElementVNode("div", _hoisted_6, toDisplayString(sm.value.resource), 1)]), createElementVNode("div", {
1133
+ ref_key: "tableBody",
1134
+ ref: tableBody,
1135
+ class: "dou-gantt__table-body",
1136
+ tabindex: "-1",
1137
+ onScroll: _cache[1] || (_cache[1] = (...args) => unref(axis).onTableScroll && unref(axis).onTableScroll(...args))
1138
+ }, [createElementVNode("div", {
1139
+ class: "dou-gantt__table-rows",
1140
+ style: normalizeStyle({ height: `${totalHeight.value + unref(scrollbar)}px` })
1141
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleRows.value, (row) => {
1142
+ return openBlock(), createElementBlock("div", {
1143
+ key: row.key,
1144
+ class: normalizeClass(["dou-gantt__tr", {
1145
+ "is-group": row.kind === "group",
1146
+ "is-hovered": hovered.value === row.key,
1147
+ "is-overloaded": row.kind === "resource" && row.overloaded
1148
+ }]),
1149
+ style: normalizeStyle({
1150
+ transform: `translateY(${row.top}px)`,
1151
+ height: `${row.height}px`
1152
+ }),
1153
+ "data-row": row.key,
1154
+ role: "listitem",
1155
+ "aria-label": row.kind === "resource" ? describeRow(row) : row.title,
1156
+ onPointerenter: ($event) => hovered.value = row.key,
1157
+ onPointerleave: _cache[0] || (_cache[0] = ($event) => hovered.value = null)
1158
+ }, [createElementVNode("div", _hoisted_8, [row.kind === "group" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("button", {
1159
+ type: "button",
1160
+ class: "dou-gantt__toggle",
1161
+ tabindex: "-1",
1162
+ "aria-label": collapsed.value.has(row.key) ? unref(messages).expand : unref(messages).collapse,
1163
+ "aria-expanded": !collapsed.value.has(row.key),
1164
+ onClick: ($event) => toggle(row)
1165
+ }, [(openBlock(), createBlock(resolveDynamicComponent(collapsed.value.has(row.key) ? unref(ChevronRightIcon) : unref(ChevronDownIcon))))], 8, _hoisted_9), createElementVNode("span", _hoisted_10, toDisplayString(row.title), 1)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
1166
+ row.resource?.group ? (openBlock(), createElementBlock("span", _hoisted_11)) : createCommentVNode("", true),
1167
+ _cache[5] || (_cache[5] = createElementVNode("span", { class: "dou-gantt__toggle-space" }, null, -1)),
1168
+ createElementVNode("span", { class: normalizeClass(["dou-gantt__cell-text", { "is-unassigned": !row.resource }]) }, [row.resource ? renderSlot(_ctx.$slots, "resource", { resource: row.resource }, () => [createTextVNode(toDisplayString(row.title), 1)], void 0, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(row.title), 1)], 64))], 2),
1169
+ row.overloaded ? (openBlock(), createElementBlock("span", {
1170
+ key: 1,
1171
+ class: "dou-gantt__overload",
1172
+ title: sm.value.overloadHint,
1173
+ "aria-hidden": "true"
1174
+ }, null, 8, _hoisted_12)) : createCommentVNode("", true)
1175
+ ], 64))])], 46, _hoisted_7);
1176
+ }), 128))], 4)], 544)], 8, _hoisted_4)], 4)) : createCommentVNode("", true),
1177
+ createElementVNode("div", {
1178
+ ref_key: "chart",
1179
+ ref: chart,
1180
+ class: normalizeClass(["dou-gantt__chart", { "is-fixed": __props.range }]),
1181
+ tabindex: "-1",
1182
+ onScrollPassive: _cache[2] || (_cache[2] = (...args) => unref(axis).onChartScroll && unref(axis).onChartScroll(...args)),
1183
+ onWheel: _cache[3] || (_cache[3] = (...args) => unref(axis).onWheel && unref(axis).onWheel(...args))
1184
+ }, [createElementVNode("div", {
1185
+ class: "dou-gantt__canvas",
1186
+ style: normalizeStyle({ width: `${unref(totalWidth)}px` })
1187
+ }, [createVNode(time_header_default, {
1188
+ "top-cells": unref(topCells),
1189
+ "bottom-cells": unref(bottomCells),
1190
+ "bottom-unit": unref(level).bottom,
1191
+ calendar: calendar.value,
1192
+ now: unref(now),
1193
+ "today-x": unref(todayX)
1194
+ }, {
1195
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(markers.value, (item) => {
1196
+ return openBlock(), createElementBlock("span", {
1197
+ key: item.key,
1198
+ class: normalizeClass(["dou-gantt__marker-label", [item.marker.tone && `is-${item.marker.tone}`, {
1199
+ "is-cut": item.room !== Infinity,
1200
+ "is-crowded": !item.room
1201
+ }]]),
1202
+ title: item.room !== Infinity ? item.marker.label : void 0,
1203
+ style: normalizeStyle({
1204
+ left: `${item.x}px`,
1205
+ maxWidth: item.room && item.room !== Infinity ? `${item.room}px` : void 0
1206
+ })
1207
+ }, [item.marker.label ? (openBlock(), createElementBlock("span", _hoisted_14, toDisplayString(item.marker.label), 1)) : createCommentVNode("", true)], 14, _hoisted_13);
1208
+ }), 128))]),
1209
+ _: 1
1210
+ }, 8, [
1211
+ "top-cells",
1212
+ "bottom-cells",
1213
+ "bottom-unit",
1214
+ "calendar",
1215
+ "now",
1216
+ "today-x"
1217
+ ]), createElementVNode("div", {
1218
+ class: "dou-gantt__body",
1219
+ style: normalizeStyle({ height: `${totalHeight.value}px` }),
1220
+ onPointerdown: onBodyDown,
1221
+ onContextmenu: onBodyContextmenu
1222
+ }, [
1223
+ !showTable.value ? (openBlock(), createElementBlock("div", _hoisted_15, [(openBlock(true), createElementBlock(Fragment, null, renderList(rowTitles.value, (row) => {
1224
+ return openBlock(), createElementBlock("span", {
1225
+ key: row.key,
1226
+ class: normalizeClass(["dou-gantt__row-title", { "is-summary": row.kind === "group" }]),
1227
+ style: normalizeStyle({ transform: `translateY(${row.top}px)` })
1228
+ }, [row.kind === "resource" && row.resource ? renderSlot(_ctx.$slots, "resource", { resource: row.resource }, () => [createTextVNode(toDisplayString(row.title), 1)], void 0, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(row.title), 1)], 64))], 6);
1229
+ }), 128))])) : createCommentVNode("", true),
1230
+ createVNode(time_grid_default, {
1231
+ cells: unref(bottomCells),
1232
+ shaded: unref(shaded)
1233
+ }, null, 8, ["cells", "shaded"]),
1234
+ (openBlock(true), createElementBlock(Fragment, null, renderList(visibleRows.value, (row) => {
1235
+ return openBlock(), createElementBlock("div", {
1236
+ key: row.key,
1237
+ class: normalizeClass(["dou-gantt__lane-row", {
1238
+ "is-group": row.kind === "group",
1239
+ "is-hovered": hovered.value === row.key
1240
+ }]),
1241
+ style: normalizeStyle({
1242
+ transform: `translateY(${row.top}px)`,
1243
+ height: `${row.height}px`
1244
+ })
1245
+ }, null, 6);
1246
+ }), 128)),
1247
+ unref(todayX) != null ? (openBlock(), createElementBlock("div", {
1248
+ key: 1,
1249
+ class: "dou-gantt__today",
1250
+ style: normalizeStyle({ left: `${unref(todayX)}px` })
1251
+ }, null, 4)) : createCommentVNode("", true),
1252
+ (openBlock(true), createElementBlock(Fragment, null, renderList(markers.value, (item) => {
1253
+ return openBlock(), createElementBlock("div", {
1254
+ key: item.key,
1255
+ class: normalizeClass(["dou-gantt__marker", item.marker.tone && `is-${item.marker.tone}`]),
1256
+ style: normalizeStyle({ left: `${item.x}px` })
1257
+ }, null, 6);
1258
+ }), 128)),
1259
+ (openBlock(true), createElementBlock(Fragment, null, renderList(loads.value, (segment) => {
1260
+ return openBlock(), createElementBlock("div", {
1261
+ key: segment.key,
1262
+ class: normalizeClass(["dou-gantt__load", { "is-over": segment.over }]),
1263
+ style: normalizeStyle({
1264
+ transform: `translate(${segment.left}px, ${segment.top}px)`,
1265
+ width: `${segment.width}px`,
1266
+ "--dou-gantt-load": segment.level
1267
+ })
1268
+ }, null, 6);
1269
+ }), 128)),
1270
+ (openBlock(true), createElementBlock(Fragment, null, renderList(bars.value, (bar) => {
1271
+ return openBlock(), createElementBlock("div", {
1272
+ key: bar.item.task.id,
1273
+ class: normalizeClass(["dou-gantt__bar-row", {
1274
+ "is-selected": selected.value.has(bar.item.task.id),
1275
+ "is-active": drag.value?.started && drag.value.spans.has(bar.item.task.id)
1276
+ }]),
1277
+ style: normalizeStyle({ transform: `translateY(${bar.top}px)` })
1278
+ }, [
1279
+ createElementVNode("div", {
1280
+ class: normalizeClass(["dou-gantt__bar", [bar.item.task.tone && `is-${bar.item.task.tone}`, {
1281
+ "is-milestone": bar.milestone,
1282
+ "is-selected": selected.value.has(bar.item.task.id),
1283
+ "is-locked": bar.item.task.locked,
1284
+ "is-active": active.value === bar.item.task.id,
1285
+ "is-move-refused": !!dragRefusal.value && drag.value?.started && drag.value.spans.has(bar.item.task.id)
1286
+ }]]),
1287
+ "data-task": bar.item.task.id,
1288
+ style: normalizeStyle({
1289
+ left: `${bar.left}px`,
1290
+ width: bar.milestone ? void 0 : `${bar.width}px`
1291
+ }),
1292
+ role: "img",
1293
+ tabindex: "-1",
1294
+ "aria-label": bar.aria,
1295
+ onPointerdown: ($event) => onBarDown($event, bar),
1296
+ onClick: ($event) => onTaskClick(bar.item.task, $event),
1297
+ onDblclick: ($event) => emit("taskDblclick", bar.item.task, $event),
1298
+ onContextmenu: ($event) => onTaskContextmenu(bar, $event)
1299
+ }, [!bar.outside ? (openBlock(), createElementBlock("span", _hoisted_17, [renderSlot(_ctx.$slots, "bar", { task: bar.item.task }, () => [createTextVNode(toDisplayString(bar.item.task.title), 1)])])) : createCommentVNode("", true)], 46, _hoisted_16),
1300
+ editable.value && !bar.item.task.locked && !bar.milestone ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", {
1301
+ class: "dou-gantt__handle is-start",
1302
+ "data-handle": "start",
1303
+ style: normalizeStyle({ left: `${bar.left}px` }),
1304
+ onPointerdown: ($event) => onBarDown($event, bar)
1305
+ }, null, 44, _hoisted_18), createElementVNode("span", {
1306
+ class: "dou-gantt__handle is-end",
1307
+ "data-handle": "end",
1308
+ style: normalizeStyle({ left: `${bar.left + bar.width}px` }),
1309
+ onPointerdown: ($event) => onBarDown($event, bar)
1310
+ }, null, 44, _hoisted_19)], 64)) : createCommentVNode("", true),
1311
+ bar.outside ? (openBlock(), createElementBlock("span", {
1312
+ key: 1,
1313
+ class: "dou-gantt__bar-side",
1314
+ style: normalizeStyle({ left: `${bar.left + (bar.milestone ? 12 : bar.width + 8)}px` })
1315
+ }, [renderSlot(_ctx.$slots, "bar", { task: bar.item.task }, () => [createTextVNode(toDisplayString(bar.item.task.title), 1)])], 4)) : createCommentVNode("", true)
1316
+ ], 6);
1317
+ }), 128)),
1318
+ drawnBox.value ? (openBlock(), createElementBlock("div", {
1319
+ key: 2,
1320
+ class: "dou-gantt__ghost",
1321
+ style: normalizeStyle({
1322
+ transform: `translate(${drawnBox.value.left}px, ${drawnBox.value.top}px)`,
1323
+ width: `${drawnBox.value.width}px`
1324
+ })
1325
+ }, null, 4)) : createCommentVNode("", true),
1326
+ dragTip.value ? (openBlock(), createElementBlock("div", {
1327
+ key: 3,
1328
+ class: normalizeClass(["dou-gantt__tip", { "is-refused": dragTip.value.refused }]),
1329
+ style: normalizeStyle({ transform: `translate(${dragTip.value.left}px, ${dragTip.value.top}px)` })
1330
+ }, toDisplayString(dragTip.value.text), 7)) : createCommentVNode("", true)
1331
+ ], 36)], 4)], 34),
1332
+ showTable.value ? (openBlock(), createElementBlock("div", {
1333
+ key: 1,
1334
+ class: "dou-gantt__divider",
1335
+ role: "separator",
1336
+ "aria-label": unref(messages).resizeTable,
1337
+ tabindex: "0",
1338
+ "aria-orientation": "vertical",
1339
+ "aria-valuenow": Math.round(tableSize.value),
1340
+ "aria-valuemin": TABLE_MIN,
1341
+ "aria-valuemax": tableMax.value,
1342
+ onPointerdown: onDividerDown,
1343
+ onKeydown: onDividerKey
1344
+ }, null, 40, _hoisted_20)) : createCommentVNode("", true),
1345
+ createVNode(gantt_menu_default, {
1346
+ ref_key: "menu",
1347
+ ref: menu,
1348
+ open: menuOpen.value,
1349
+ "onUpdate:open": _cache[4] || (_cache[4] = ($event) => menuOpen.value = $event),
1350
+ items: menuItems.value
1351
+ }, null, 8, ["open", "items"]),
1352
+ renderSlot(_ctx.$slots, "default")
1353
+ ], 46, _hoisted_1);
1354
+ };
1355
+ }
1356
+ });
1357
+ export { scheduler_vue_vue_type_script_setup_true_lang_default as default };
1358
+
1359
+ //# sourceMappingURL=scheduler.vue_vue_type_script_setup_true_lang.js.map