@workiom/frappe-gantt 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -449,6 +449,9 @@ export default class Gantt {
449
449
  this.bind_bar_events();
450
450
  this.bind_task_column_resize();
451
451
  this.bind_task_column_scroll();
452
+ if (this.options.allow_dependency_creation) {
453
+ this.bind_dependency_linking();
454
+ }
452
455
  }
453
456
 
454
457
  render() {
@@ -474,6 +477,12 @@ export default class Gantt {
474
477
  append_to: this.$svg,
475
478
  });
476
479
  }
480
+ if (this.options.allow_dependency_creation) {
481
+ this.layers.linking = createSVG('g', {
482
+ class: 'linking',
483
+ append_to: this.$svg,
484
+ });
485
+ }
477
486
  this.$extras = this.create_el({
478
487
  classes: 'extras',
479
488
  append_to: this.$container,
@@ -1078,6 +1087,7 @@ export default class Gantt {
1078
1087
 
1079
1088
  make_arrows() {
1080
1089
  this.arrows = [];
1090
+ this.active_arrow = null;
1081
1091
 
1082
1092
  // Calculate critical path if enabled
1083
1093
  if (this.options.critical_path) {
@@ -1346,7 +1356,7 @@ export default class Gantt {
1346
1356
  '.grid-row, .grid-header, .ignored-bar, .holiday-highlight',
1347
1357
  (e, delegatedTarget) => {
1348
1358
  // Check if click is on a grid-row (not header or other elements)
1349
- if (delegatedTarget && delegatedTarget.classList.contains('grid-row')) {
1359
+ if (delegatedTarget && (delegatedTarget.classList.contains('grid-row') || delegatedTarget.classList.contains('ignored-bar') || delegatedTarget.classList.contains('holiday-highlight'))) {
1350
1360
  // Get the click position relative to the SVG
1351
1361
  const svg = this.$svg;
1352
1362
  const pt = svg.createSVGPoint();
@@ -1370,6 +1380,13 @@ export default class Gantt {
1370
1380
  this.config.unit
1371
1381
  );
1372
1382
 
1383
+ // If weekend/ignored skipping is enabled, advance to next non-ignored day
1384
+ if (this.config.ignored_function) {
1385
+ while (this.config.ignored_function(clicked_date)) {
1386
+ clicked_date = date_utils.add(clicked_date, 1, 'day');
1387
+ }
1388
+ }
1389
+
1373
1390
  // Set start date to clicked date and end date to 1 day after
1374
1391
  task._start = clicked_date;
1375
1392
  task._end = date_utils.add(clicked_date, 1, 'day');
@@ -1454,6 +1471,7 @@ export default class Gantt {
1454
1471
  let parent_bar_id = null;
1455
1472
  let bars = []; // instanceof Bar
1456
1473
  this.bar_being_dragged = null;
1474
+ this.active_arrow = null;
1457
1475
 
1458
1476
  const action_in_progress = () =>
1459
1477
  is_dragging || is_resizing_left || is_resizing_right;
@@ -1462,6 +1480,11 @@ export default class Gantt {
1462
1480
  if (e.target.classList.contains('grid-row')) this.unselect_all();
1463
1481
  };
1464
1482
 
1483
+ if (!this._document_click_handler) {
1484
+ this._document_click_handler = () => this.set_active_arrow(null);
1485
+ document.addEventListener('click', this._document_click_handler);
1486
+ }
1487
+
1465
1488
  let pos = 0;
1466
1489
  $.on(this.$svg, 'mousemove', '.bar-wrapper, .handle', (e) => {
1467
1490
  if (
@@ -1472,6 +1495,7 @@ export default class Gantt {
1472
1495
  });
1473
1496
 
1474
1497
  $.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => {
1498
+ if (e.target.classList.contains('connector-circle')) return;
1475
1499
  const bar_wrapper = $.closest('.bar-wrapper', element);
1476
1500
  if (element.classList.contains('left')) {
1477
1501
  is_resizing_left = true;
@@ -1938,7 +1962,6 @@ export default class Gantt {
1938
1962
  }
1939
1963
 
1940
1964
  let dx = now_x - x_on_start;
1941
- console.log($bar_progress);
1942
1965
  if (dx > $bar_progress.max_dx) {
1943
1966
  dx = $bar_progress.max_dx;
1944
1967
  }
@@ -1965,6 +1988,229 @@ export default class Gantt {
1965
1988
  });
1966
1989
  }
1967
1990
 
1991
+ bind_dependency_linking() {
1992
+ this.is_linking = false;
1993
+ this.linking_source_bar = null;
1994
+ this.linking_source_endpoint = null;
1995
+ this.linking_temp_line = null;
1996
+ this.linking_snap_badge = null;
1997
+
1998
+ $.on(this.$svg, 'mousedown', '.connector-circle', (e, circle) => {
1999
+ const bar_wrapper = $.closest('.bar-wrapper', circle);
2000
+ if (!bar_wrapper) return;
2001
+ const bar_id = bar_wrapper.getAttribute('data-id');
2002
+ const source_bar = this.get_bar(bar_id);
2003
+ if (!source_bar) return;
2004
+
2005
+ this.is_linking = true;
2006
+ this.linking_source_bar = source_bar;
2007
+ this.linking_source_endpoint = circle.getAttribute('data-endpoint');
2008
+
2009
+ const cx = parseFloat(circle.getAttribute('cx'));
2010
+ const cy = parseFloat(circle.getAttribute('cy'));
2011
+
2012
+ this.linking_temp_line = createSVG('line', {
2013
+ x1: cx,
2014
+ y1: cy,
2015
+ x2: cx,
2016
+ y2: cy,
2017
+ class: 'linking-temp-line',
2018
+ append_to: this.layers.linking,
2019
+ });
2020
+ });
2021
+
2022
+ $.on(this.$svg, 'mousemove', (e) => {
2023
+ if (!this.is_linking || !this.linking_temp_line) return;
2024
+ const pt = this.$svg.createSVGPoint();
2025
+ pt.x = e.clientX;
2026
+ pt.y = e.clientY;
2027
+ const svgP = pt.matrixTransform(this.$svg.getScreenCTM().inverse());
2028
+ this.linking_temp_line.setAttribute('x2', svgP.x);
2029
+ this.linking_temp_line.setAttribute('y2', svgP.y);
2030
+ });
2031
+
2032
+ $.on(this.$svg, 'mouseover', '.connector-circle', (e, circle) => {
2033
+ if (!this.is_linking) return;
2034
+ const bar_wrapper = $.closest('.bar-wrapper', circle);
2035
+ if (!bar_wrapper) return;
2036
+ const bar_id = bar_wrapper.getAttribute('data-id');
2037
+ if (bar_id === this.linking_source_bar.task.id) return;
2038
+
2039
+ circle.setAttribute('r', '9');
2040
+ if (this.linking_temp_line) {
2041
+ this.linking_temp_line.classList.add('snap');
2042
+ }
2043
+
2044
+ if (!this.linking_snap_badge) {
2045
+ const to_ep = circle.getAttribute('data-endpoint');
2046
+ const type = this._resolve_dependency_type(
2047
+ this.linking_source_endpoint,
2048
+ to_ep,
2049
+ );
2050
+ const abbr = { 'finish-to-start': 'FS', 'start-to-start': 'SS', 'finish-to-finish': 'FF', 'start-to-finish': 'SF' }[type] || 'FS';
2051
+ const cx = parseFloat(circle.getAttribute('cx'));
2052
+ const cy = parseFloat(circle.getAttribute('cy'));
2053
+ this.linking_snap_badge = createSVG('text', {
2054
+ x: cx + 12,
2055
+ y: cy - 10,
2056
+ class: 'linking-snap-badge',
2057
+ append_to: this.layers.linking,
2058
+ });
2059
+ this.linking_snap_badge.textContent = abbr;
2060
+ }
2061
+ });
2062
+
2063
+ $.on(this.$svg, 'mouseout', '.connector-circle', (e, circle) => {
2064
+ if (!this.is_linking) return;
2065
+ circle.setAttribute('r', '4');
2066
+ if (this.linking_temp_line) {
2067
+ this.linking_temp_line.classList.remove('snap');
2068
+ }
2069
+ if (this.linking_snap_badge) {
2070
+ this.linking_snap_badge.remove();
2071
+ this.linking_snap_badge = null;
2072
+ }
2073
+ });
2074
+
2075
+ $.on(this.$svg, 'mouseup', '.connector-circle', (e, circle) => {
2076
+ if (!this.is_linking) return;
2077
+ const bar_wrapper = $.closest('.bar-wrapper', circle);
2078
+ if (!bar_wrapper) return;
2079
+ const bar_id = bar_wrapper.getAttribute('data-id');
2080
+ if (bar_id !== this.linking_source_bar.task.id) {
2081
+ const to_bar = this.get_bar(bar_id);
2082
+ const to_endpoint = circle.getAttribute('data-endpoint');
2083
+ if (to_bar) {
2084
+ this._commit_dependency(to_bar, to_endpoint);
2085
+ }
2086
+ }
2087
+ // _cancel_linking() is called by the document mouseup handler
2088
+ });
2089
+
2090
+ document.addEventListener('mouseup', () => {
2091
+ if (!this.is_linking) return;
2092
+ this._cancel_linking();
2093
+ });
2094
+
2095
+ document.addEventListener('keydown', (e) => {
2096
+ if (
2097
+ (e.key === 'Delete' || e.key === 'Backspace') &&
2098
+ this.active_arrow
2099
+ ) {
2100
+ const tag = e.target.tagName;
2101
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return;
2102
+ e.preventDefault();
2103
+ this.delete_dependency(this.active_arrow);
2104
+ }
2105
+ });
2106
+ }
2107
+
2108
+ _resolve_dependency_type(from_endpoint, to_endpoint) {
2109
+ if (from_endpoint === 'end' && to_endpoint === 'start') return 'finish-to-start';
2110
+ if (from_endpoint === 'start' && to_endpoint === 'start') return 'start-to-start';
2111
+ if (from_endpoint === 'end' && to_endpoint === 'end') return 'finish-to-finish';
2112
+ if (from_endpoint === 'start' && to_endpoint === 'end') return 'start-to-finish';
2113
+ return 'finish-to-start';
2114
+ }
2115
+
2116
+ _commit_dependency(to_bar, to_endpoint) {
2117
+ const from_task = this.linking_source_bar.task;
2118
+ const to_task = to_bar.task;
2119
+ const type = this._resolve_dependency_type(
2120
+ this.linking_source_endpoint,
2121
+ to_endpoint,
2122
+ );
2123
+
2124
+ // Find any existing dependency from the same source task (any type)
2125
+ const existing = to_task.dependencies.find((d) => d.id === from_task.id);
2126
+ const existing_type = existing
2127
+ ? existing.type || this.options.dependencies_type || 'finish-to-start'
2128
+ : null;
2129
+
2130
+ // Remove existing connection between this pair (if any)
2131
+ const deps_without_existing = to_task.dependencies.filter(
2132
+ (d) => d.id !== from_task.id,
2133
+ );
2134
+
2135
+ if (existing_type === type) {
2136
+ // Same type drawn again — toggle off (just remove)
2137
+ this.update_task(to_task.id, { dependencies: deps_without_existing });
2138
+ if (this.options.on_dependency_delete) {
2139
+ this.options.on_dependency_delete(from_task, to_task, type);
2140
+ }
2141
+ return;
2142
+ }
2143
+
2144
+ // Remove reverse dependency if it exists (prevents cycles)
2145
+ const reverse = from_task.dependencies?.find((d) => d.id === to_task.id);
2146
+ if (reverse) {
2147
+ const reverse_type =
2148
+ reverse.type || this.options.dependencies_type || 'finish-to-start';
2149
+ this.update_task(from_task.id, {
2150
+ dependencies: from_task.dependencies.filter((d) => d.id !== to_task.id),
2151
+ });
2152
+ if (this.options.on_dependency_delete) {
2153
+ this.options.on_dependency_delete(to_task, from_task, reverse_type);
2154
+ }
2155
+ }
2156
+
2157
+ // Different type (or no prior connection) — replace with new
2158
+ const new_deps = [...deps_without_existing, { id: from_task.id, type }];
2159
+ this.update_task(to_task.id, { dependencies: new_deps });
2160
+
2161
+ if (existing) {
2162
+ if (this.options.on_dependency_changed) {
2163
+ this.options.on_dependency_changed(
2164
+ from_task,
2165
+ to_task,
2166
+ existing_type,
2167
+ type,
2168
+ );
2169
+ }
2170
+ } else {
2171
+ if (this.options.on_dependency_create) {
2172
+ this.options.on_dependency_create(from_task, to_task, type);
2173
+ }
2174
+ }
2175
+ }
2176
+
2177
+ _cancel_linking() {
2178
+ if (this.linking_temp_line) {
2179
+ this.linking_temp_line.remove();
2180
+ this.linking_temp_line = null;
2181
+ }
2182
+ if (this.linking_snap_badge) {
2183
+ this.linking_snap_badge.remove();
2184
+ this.linking_snap_badge = null;
2185
+ }
2186
+ this.$svg
2187
+ .querySelectorAll('.connector-circle[r="9"]')
2188
+ .forEach((el) => el.setAttribute('r', '4'));
2189
+
2190
+ this.is_linking = false;
2191
+ this.linking_source_bar = null;
2192
+ this.linking_source_endpoint = null;
2193
+ }
2194
+
2195
+ delete_dependency(arrow) {
2196
+ const from_task = arrow.from_task.task;
2197
+ const to_task = arrow.to_task.task;
2198
+ const arrow_type = arrow.dependency_type;
2199
+
2200
+ const new_deps = to_task.dependencies.filter((d) => {
2201
+ const dep_type =
2202
+ d.type || this.options.dependencies_type || 'finish-to-start';
2203
+ return !(d.id === from_task.id && dep_type === arrow_type);
2204
+ });
2205
+
2206
+ this.set_active_arrow(null);
2207
+ this.update_task(to_task.id, { dependencies: new_deps });
2208
+
2209
+ if (this.options.on_dependency_delete) {
2210
+ this.options.on_dependency_delete(from_task, to_task, arrow_type);
2211
+ }
2212
+ }
2213
+
1968
2214
  get_all_dependent_tasks(task_id) {
1969
2215
  let out = [];
1970
2216
  let to_process = [task_id];
@@ -2026,6 +2272,20 @@ export default class Gantt {
2026
2272
  }
2027
2273
  }
2028
2274
 
2275
+ set_active_arrow(arrow) {
2276
+ if (this.active_arrow === arrow) return;
2277
+ if (this.active_arrow) {
2278
+ this.active_arrow.deactivate();
2279
+ }
2280
+ this.active_arrow = arrow;
2281
+ if (arrow) {
2282
+ arrow.activate();
2283
+ if (this.options.on_arrow_click) {
2284
+ this.options.on_arrow_click(arrow.from_task.task, arrow.to_task.task);
2285
+ }
2286
+ }
2287
+ }
2288
+
2029
2289
  unselect_all() {
2030
2290
  if (this.popup) this.popup.parent.classList.add('hide');
2031
2291
  this.$container
@@ -11,8 +11,14 @@
11
11
  --g-arrow-hover-color-dark: #60a5fa;
12
12
  --g-arrow-critical-color-dark: #f5c044;
13
13
  --g-arrow-invalid-color-dark: #ff7676;
14
+ --g-arrow-type-label-bg-dark: #4a4a4a;
15
+ --g-arrow-type-label-color-dark: #fff;
14
16
  --g-resize-handle-hover-dark: rgba(96, 165, 250, 0.4);
15
17
  --g-resize-handle-active-dark: rgba(96, 165, 250, 0.6);
18
+ --g-connector-start-color-dark: #4ade80;
19
+ --g-connector-end-color-dark: #fb923c;
20
+ --g-connector-linking-color-dark: #818cf8;
21
+ --g-connector-fill-dark: #ffffff;
16
22
  }
17
23
 
18
24
  .dark > .gantt-container .gantt {
@@ -44,11 +50,27 @@
44
50
  stroke: var(--g-arrow-hover-color-dark);
45
51
  }
46
52
 
53
+ & .arrow-active {
54
+ stroke: var(--g-arrow-hover-color-dark);
55
+ }
56
+
57
+ & .arrow-type-label rect {
58
+ fill: var(--g-arrow-type-label-bg-dark);
59
+ }
60
+
61
+ & .arrow-type-label text {
62
+ fill: var(--g-arrow-type-label-color-dark);
63
+ }
64
+
47
65
  & .bar {
48
66
  fill: var(--g-bar-color-dark);
49
67
  stroke: none;
50
68
  }
51
69
 
70
+ & .bar-wrapper .bar.bar-arrow-active {
71
+ outline-color: var(--g-arrow-hover-color-dark);
72
+ }
73
+
52
74
  & .bar-progress {
53
75
  fill: var(--g-progress-color);
54
76
  }
@@ -87,6 +109,11 @@
87
109
  }
88
110
  }
89
111
  }
112
+
113
+ --g-connector-start-color: var(--g-connector-start-color-dark);
114
+ --g-connector-end-color: var(--g-connector-end-color-dark);
115
+ --g-connector-linking-color: var(--g-connector-linking-color-dark);
116
+ --g-connector-fill: var(--g-connector-fill-dark);
90
117
  }
91
118
 
92
119
  .dark > .gantt-container {
@@ -291,6 +291,26 @@
291
291
  stroke: var(--g-arrow-hover-color);
292
292
  }
293
293
 
294
+ & .arrow-active {
295
+ stroke: var(--g-arrow-hover-color);
296
+ }
297
+
298
+ & .arrow-type-label {
299
+ stroke-width: 0;
300
+ cursor: pointer;
301
+ }
302
+
303
+ & .arrow-type-label rect {
304
+ fill: var(--g-arrow-type-label-bg);
305
+ }
306
+
307
+ & .arrow-type-label text {
308
+ fill: var(--g-arrow-type-label-color);
309
+ font-size: 10px;
310
+ font-weight: 600;
311
+ font-family: inherit;
312
+ pointer-events: none;
313
+ }
294
314
 
295
315
  & .bar-wrapper .bar {
296
316
  fill: var(--g-bar-color);
@@ -299,6 +319,12 @@
299
319
  transition: stroke-width 0.3s ease;
300
320
  }
301
321
 
322
+ & .bar-wrapper .bar.bar-arrow-active {
323
+ outline-color: var(--g-arrow-hover-color);
324
+ outline-width: 2px;
325
+ outline-style: solid;
326
+ }
327
+
302
328
  & .bar-wrapper .bar.bar-arrow-critical {
303
329
  outline-color: var(--g-arrow-critical-color);
304
330
  }
@@ -372,6 +398,28 @@
372
398
  display: block;
373
399
  }
374
400
  }
401
+
402
+ & .connector-circle {
403
+ fill: var(--g-connector-fill);
404
+ opacity: 0;
405
+ cursor: crosshair;
406
+ transition: opacity 0.2s ease;
407
+ pointer-events: all;
408
+ }
409
+
410
+ & .connector-circle.connector-start {
411
+ stroke: var(--g-connector-start-color);
412
+ stroke-width: 2;
413
+ }
414
+
415
+ & .connector-circle.connector-end {
416
+ stroke: var(--g-connector-end-color);
417
+ stroke-width: 2;
418
+ }
419
+
420
+ &:hover .connector-circle {
421
+ opacity: 1;
422
+ }
375
423
  }
376
424
 
377
425
  & .add-task-icon {
@@ -404,6 +452,27 @@
404
452
  }
405
453
  }
406
454
  }
455
+
456
+ & .linking-temp-line {
457
+ stroke: var(--g-connector-linking-color);
458
+ stroke-width: 2;
459
+ stroke-dasharray: 5, 4;
460
+ fill: none;
461
+ pointer-events: none;
462
+ }
463
+
464
+ & .linking-temp-line.snap {
465
+ stroke: var(--g-connector-start-color);
466
+ stroke-dasharray: 0;
467
+ }
468
+
469
+ & .linking-snap-badge {
470
+ font-size: 11px;
471
+ font-weight: 700;
472
+ fill: var(--g-connector-start-color);
473
+ pointer-events: none;
474
+ user-select: none;
475
+ }
407
476
  }
408
477
 
409
478
  /* Task Name Column */
@@ -21,9 +21,15 @@
21
21
  --g-row-border-color: #c7c7c7;
22
22
  --g-today-highlight: #37352f;
23
23
  --g-popup-actions: #ebeff2;
24
+ --g-arrow-type-label-bg: #dbdbdb;
25
+ --g-arrow-type-label-color: #000;
24
26
  --g-weekend-highlight-color: #f7f7f7;
25
27
  --g-task-column-bg: #ffffff;
26
28
  --g-task-row-bg: #ffffff;
27
29
  --g-resize-handle-hover: rgba(59, 130, 246, 0.3);
28
30
  --g-resize-handle-active: rgba(59, 130, 246, 0.5);
31
+ --g-connector-start-color: #22c55e;
32
+ --g-connector-end-color: #f97316;
33
+ --g-connector-linking-color: #6366f1;
34
+ --g-connector-fill: #ffffff;
29
35
  }