Project

General

Profile

Patch #43397 » 0002-Extract-Gantt-view-structure-and-wire-Stimulus-controllers.patch

Katsuya HIDAKA, 2025-10-26 18:24

View differences:

app/assets/javascripts/gantt.js
1
/**
2
 * Redmine - project management software
3
 * Copyright (C) 2006-  Jean-Philippe Lang
4
 * This code is released under the GNU General Public License.
5
 */
6

  
7
var draw_gantt = null;
8
var draw_top;
9
var draw_right;
10
var draw_left;
11

  
12
var rels_stroke_width = 2;
13

  
14
function setDrawArea() {
15
  draw_top   = $("#gantt_draw_area").position().top;
16
  draw_right = $("#gantt_draw_area").width();
17
  draw_left  = $("#gantt_area").scrollLeft();
18
}
19

  
20
function getRelationsArray() {
21
  var arr = new Array();
22
  $.each($('div.task_todo[data-rels]'), function(index_div, element) {
23
    if(!$(element).is(':visible')) return true;
24
    var element_id = $(element).attr("id");
25
    if (element_id != null) {
26
      var issue_id = element_id.replace("task-todo-issue-", "");
27
      var data_rels = $(element).data("rels");
28
      for (rel_type_key in data_rels) {
29
        $.each(data_rels[rel_type_key], function(index_issue, element_issue) {
30
          arr.push({issue_from: issue_id, issue_to: element_issue,
31
                    rel_type: rel_type_key});
32
        });
33
      }
34
    }
35
  });
36
  return arr;
37
}
38

  
39
function drawRelations() {
40
  var arr = getRelationsArray();
41
  $.each(arr, function(index_issue, element_issue) {
42
    var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
43
    var issue_to   = $("#task-todo-issue-" + element_issue["issue_to"]);
44
    if (issue_from.length == 0 || issue_to.length == 0) {
45
      return;
46
    }
47
    var issue_height = issue_from.height();
48
    var issue_from_top   = issue_from.position().top  + (issue_height / 2) - draw_top;
49
    var issue_from_right = issue_from.position().left + issue_from.width();
50
    var issue_to_top   = issue_to.position().top  + (issue_height / 2) - draw_top;
51
    var issue_to_left  = issue_to.position().left;
52
    var color = issue_relation_type[element_issue["rel_type"]]["color"];
53
    var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
54
    var issue_from_right_rel = issue_from_right + landscape_margin;
55
    var issue_to_left_rel    = issue_to_left    - landscape_margin;
56
    draw_gantt.path(["M", issue_from_right + draw_left,     issue_from_top,
57
                     "L", issue_from_right_rel + draw_left, issue_from_top])
58
                   .attr({stroke: color,
59
                          "stroke-width": rels_stroke_width
60
                          });
61
    if (issue_from_right_rel < issue_to_left_rel) {
62
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
63
                       "L", issue_from_right_rel + draw_left, issue_to_top])
64
                     .attr({stroke: color,
65
                          "stroke-width": rels_stroke_width
66
                          });
67
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
68
                       "L", issue_to_left + draw_left,        issue_to_top])
69
                     .attr({stroke: color,
70
                          "stroke-width": rels_stroke_width
71
                          });
72
    } else {
73
      var issue_middle_top = issue_to_top +
74
                                (issue_height *
75
                                   ((issue_from_top > issue_to_top) ? 1 : -1));
76
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
77
                       "L", issue_from_right_rel + draw_left, issue_middle_top])
78
                     .attr({stroke: color,
79
                          "stroke-width": rels_stroke_width
80
                          });
81
      draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
82
                       "L", issue_to_left_rel + draw_left,    issue_middle_top])
83
                     .attr({stroke: color,
84
                          "stroke-width": rels_stroke_width
85
                          });
86
      draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
87
                       "L", issue_to_left_rel + draw_left, issue_to_top])
88
                     .attr({stroke: color,
89
                          "stroke-width": rels_stroke_width
90
                          });
91
      draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
92
                       "L", issue_to_left + draw_left,     issue_to_top])
93
                     .attr({stroke: color,
94
                          "stroke-width": rels_stroke_width
95
                          });
96
    }
97
    draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
98
                     "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
99
                     "l", 0, 4 * rels_stroke_width, "z"])
100
                   .attr({stroke: "none",
101
                          fill: color,
102
                          "stroke-linecap": "butt",
103
                          "stroke-linejoin": "miter"
104
                          });
105
  });
106
}
107

  
108
function getProgressLinesArray() {
109
  var arr = new Array();
110
  var today_left = $('#today_line').position().left;
111
  arr.push({left: today_left, top: 0});
112
  $.each($('div.issue-subject, div.version-name'), function(index, element) {
113
    if(!$(element).is(':visible')) return true;
114
    var t = $(element).position().top - draw_top ;
115
    var h = ($(element).height() / 9);
116
    var element_top_upper  = t - h;
117
    var element_top_center = t + (h * 3);
118
    var element_top_lower  = t + (h * 8);
119
    var issue_closed   = $(element).children('span').hasClass('issue-closed');
120
    var version_closed = $(element).children('span').hasClass('version-closed');
121
    if (issue_closed || version_closed) {
122
      arr.push({left: today_left, top: element_top_center});
123
    } else {
124
      var issue_done = $("#task-done-" + $(element).attr("id"));
125
      var is_behind_start = $(element).children('span').hasClass('behind-start-date');
126
      var is_over_end     = $(element).children('span').hasClass('over-end-date');
127
      if (is_over_end) {
128
        arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
129
        arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
130
      } else if (issue_done.length > 0) {
131
        var done_left = issue_done.first().position().left +
132
                           issue_done.first().width();
133
        arr.push({left: done_left, top: element_top_center});
134
      } else if (is_behind_start) {
135
        arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
136
        arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true});
137
      } else {
138
        var todo_left = today_left;
139
        var issue_todo = $("#task-todo-" + $(element).attr("id"));
140
        if (issue_todo.length > 0){
141
          todo_left = issue_todo.first().position().left;
142
        }
143
        arr.push({left: Math.min(today_left, todo_left), top: element_top_center});
144
      }
145
    }
146
  });
147
  return arr;
148
}
149

  
150
function drawGanttProgressLines() {
151
  var arr = getProgressLinesArray();
152
  var color = $("#today_line")
153
                    .css("border-left-color");
154
  var i;
155
  for(i = 1 ; i < arr.length ; i++) {
156
    if (!("none_stroke" in arr[i]) &&
157
        (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
158
         !("is_left_edge"  in arr[i - 1] && "is_left_edge"  in arr[i]))
159
        ) {
160
      var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left;
161
      var x2 = (arr[i].left == 0)     ? 0 : arr[i].left     + draw_left;
162
      draw_gantt.path(["M", x1, arr[i - 1].top,
163
                       "L", x2, arr[i].top])
164
                   .attr({stroke: color, "stroke-width": 2});
165
    }
166
  }
167
}
168

  
169
function drawSelectedColumns(){
170
  if ($("#draw_selected_columns").prop('checked')) {
171
    if(isMobile()) {
172
      $('td.gantt_selected_column').each(function(i) {
173
        $(this).hide();
174
      });
175
    }else{
176
      $('.gantt_subjects_container').addClass('draw_selected_columns');
177
      $('td.gantt_selected_column').each(function() {
178
        $(this).show();
179
        var column_name = $(this).attr('id');
180
        $(this).resizable({
181
          zIndex: 30,
182
          alsoResize: '.gantt_' + column_name + '_container, .gantt_' + column_name + '_container > .gantt_hdr',
183
          minWidth: 20,
184
          handles: "e",
185
          create: function() {
186
            $(".ui-resizable-e").css("cursor","ew-resize");
187
          }
188
        }).on('resize', function (e) {
189
            e.stopPropagation();
190
        });
191
      });
192
    }
193
  }else{
194
    $('td.gantt_selected_column').each(function (i) {
195
      $(this).hide();
196
      $('.gantt_subjects_container').removeClass('draw_selected_columns');
197
    });
198
  }
199
}
200

  
201
function drawGanttHandler() {
202
  var folder = document.getElementById('gantt_draw_area');
203
  if(draw_gantt != null)
204
    draw_gantt.clear();
205
  else
206
    draw_gantt = Raphael(folder);
207
  setDrawArea();
208
  drawSelectedColumns();
209
  if ($("#draw_progress_line").prop('checked'))
210
    try{drawGanttProgressLines();}catch(e){}
211
  if ($("#draw_relations").prop('checked'))
212
    drawRelations();
213
  $('#content').addClass('gantt_content');
214
}
215

  
216
function resizableSubjectColumn(){
217
  $('.issue-subject, .project-name, .version-name').each(function(){
218
    $(this).width($(".gantt_subjects_column").width()-$(this).position().left);
219
  });
220
  $('td.gantt_subjects_column').resizable({
221
    alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name',
222
    minWidth: 100,
223
    handles: 'e',
224
    zIndex: 30,
225
    create: function( event, ui ) {
226
      $('.ui-resizable-e').css('cursor','ew-resize');
227
    }
228
  }).on('resize', function (e) {
229
      e.stopPropagation();
230
  });
231
  if(isMobile()) {
232
    $('td.gantt_subjects_column').resizable('disable');
233
  }else{
234
    $('td.gantt_subjects_column').resizable('enable');
235
  };
236
}
237

  
238
ganttEntryClick = function(e){
239
  var icon_expander = e.currentTarget;
240
  var subject = $(icon_expander.parentElement);
241
  var subject_left = parseInt(subject.css('left')) + parseInt(icon_expander.offsetWidth);
242
  var target_shown = null;
243
  var target_top = 0;
244
  var total_height = 0;
245
  var out_of_hierarchy = false;
246
  var iconChange = null;
247
  if(subject.hasClass('open'))
248
    iconChange = function(element){
249
      var expander = $(element).find('.expander')
250
      expander.switchClass('icon-expanded', 'icon-collapsed');
251
      $(element).removeClass('open');
252
      if (expander.find('svg').length === 1) {
253
        updateSVGIcon(expander[0], 'angle-right')
254
      }
255
    };
256
  else
257
    iconChange = function(element){
258
      var expander = $(element).find('.expander')
259
      expander.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
260
      $(element).addClass('open');
261
      if (expander.find('svg').length === 1) {
262
        updateSVGIcon(expander[0], 'angle-down')
263
      }
264
    };
265
  iconChange(subject);
266
  subject.nextAll('div').each(function(_, element){
267
    var el = $(element);
268
    var json = el.data('collapse-expand');
269
    var number_of_rows = el.data('number-of-rows');
270
    var el_task_bars = '#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
271
    var el_selected_columns = 'td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
272
    if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
273
      out_of_hierarchy = true;
274
      if(target_shown == null) return false;
275

  
276
      var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
277
      el.css('top', new_top_val);
278
      $([el_task_bars, el_selected_columns].join()).each(function(_, el){
279
        $(el).css('top', new_top_val);
280
      });
281
      return true;
282
    }
283

  
284
    var is_shown = el.is(':visible');
285
    if(target_shown == null){
286
      target_shown = is_shown;
287
      target_top = parseInt(el.css('top'));
288
      total_height = 0;
289
    }
290
    if(is_shown == target_shown){
291
      $(el_task_bars).each(function(_, task) {
292
        var el_task = $(task);
293
        if(!is_shown)
294
          el_task.css('top', target_top + total_height);
295
        if(!el_task.hasClass('tooltip'))
296
          el_task.toggle(!is_shown);
297
      });
298
      $(el_selected_columns).each(function (_, attr) {
299
        var el_attr = $(attr);
300
        if (!is_shown)
301
          el_attr.css('top', target_top + total_height);
302
          el_attr.toggle(!is_shown);
303
      });
304
      if(!is_shown)
305
        el.css('top', target_top + total_height);
306
      iconChange(el);
307
      el.toggle(!is_shown);
308
      total_height += parseInt(json.top_increment);
309
    }
310
  });
311
  drawGanttHandler();
312
};
313

  
314
function disable_unavailable_columns(unavailable_columns) {
315
  $.each(unavailable_columns, function (index, value) {
316
    $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
317
  });
318
}
app/assets/stylesheets/application.css
434 434
tr.entry.file td.filename a { margin-left: 26px; }
435 435
tr.entry.file td.filename_no_report a { margin-left: 16px; }
436 436

  
437
tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;}
438
.gantt_subjects .avatar {margin-right: 4px;}
439
.gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;}
437
tr span.expander {margin-left: 0; cursor: pointer;}
440 438

  
441 439
tr.changeset { height: 20px }
442 440
tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
......
1705 1703

  
1706 1704
#my-page .list th.checkbox, #my-page .list td.checkbox {display:none;}
1707 1705

  
1708
/***** Gantt chart *****/
1709
table.gantt-table {
1710
  width: 100%;
1711
  border-collapse: collapse;
1712
}
1713
table.gantt-table td {
1714
  padding: 0px;
1715
}
1716
.gantt_hdr {
1717
  position:absolute;
1718
  top:0;
1719
  height:16px;
1720
  border-top: 1px solid var(--oc-gray-4);
1721
  border-bottom: 1px solid var(--oc-gray-4);
1722
  border-left: 1px solid var(--oc-gray-4);
1723
  text-align: center;
1724
  overflow: hidden;
1725
}
1726
#gantt_area .gantt_hdr {
1727
  border-left: 0px;
1728
  border-right: 1px solid var(--oc-gray-4);
1729
}
1730
.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr,
1731
.last_gantt_selected_column .gantt_hdr {
1732
  border-right: 1px solid var(--oc-gray-4);
1733
}
1734
.last_gantt_selected_column .gantt_selected_column_container,
1735
.gantt_subjects_container .gantt_subjects * {
1736
  z-index: 10;
1737
}
1738

  
1739
.gantt_subjects_column + td {
1740
  padding: 0;
1741
}
1742

  
1743
.gantt_hdr.nwday {background-color:var(--oc-gray-1); color:var(--oc-gray-6);}
1744

  
1745
.gantt_subjects,
1746
.gantt_selected_column_content.gantt_hdr {
1747
  font-size: 0.8em;
1748
  position: relative;
1749
  z-index: 1;
1750
}
1751
.gantt_subjects div,
1752
.gantt_selected_column_content div {
1753
  line-height: 16px;
1754
  height: 16px;
1755
  overflow: hidden;
1756
  white-space: nowrap;
1757
  text-overflow: clip;
1758
  width: 100%;
1759
}
1760
.gantt_subjects div.issue-subject:hover { background-color:var(--oc-yellow-0); }
1761
.gantt_selected_column_content > div { padding-left: 3px; box-sizing: border-box; }
1762

  
1763
.gantt_hdr_selected_column_name {
1764
  position: absolute;
1765
  top: 50%;
1766
  width:100%;
1767
  transform: translateY(-50%);
1768
  -webkit-transform: translateY(-50%);
1769
  font-size: 0.8em;
1770
  overflow: hidden;
1771
  text-overflow: ellipsis;
1772
  white-space: nowrap;
1773

  
1774
}
1775
td.gantt_selected_column {
1776
  width: 50px;
1777
}
1778
td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
1779
  width: 49px;
1780
}
1781

  
1782
td.gantt_watcher_users_column div.issue_watcher_users ul {
1783
  margin: 0;
1784
  padding: 0;
1785
  list-style: none;
1786
}
1787

  
1788
td.gantt_watcher_users_column div.issue_watcher_users ul li {
1789
  display: inline;
1790
}
1791

  
1792
td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
1793
  content: ', ';
1794
  white-space: pre;
1795
}
1796

  
1797
.task {
1798
  position: absolute;
1799
  height:8px;
1800
  font-size:0.8em;
1801
  color:var(--oc-gray-6);
1802
  padding:0;
1803
  margin:0;
1804
  line-height:16px;
1805
  white-space:nowrap;
1806
}
1807

  
1808
.task.label {width:100%;}
1809
.task.label.project, .task.label.version { font-weight: bold; }
1810

  
1811
.task_late { background:var(--oc-red-5) url(/task_late.png); border: 1px solid var(--oc-red-5); }
1812
.task_done { background:var(--oc-green-7) url(/task_done.png); border: 1px solid var(--oc-green-7); }
1813
.task_todo { background:var(--oc-gray-5) url(/task_todo.png); border: 1px solid var(--oc-gray-5); }
1814

  
1815
.task_todo.parent { background: var(--oc-gray-6); border: 1px solid var(--oc-gray-6); height: 3px;}
1816
.task_late.parent, .task_done.parent { height: 3px;}
1817
.task.parent.marker.starting  { position: absolute; background: url(/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1818
.task.parent.marker.ending { position: absolute; background: url(/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1819

  
1820
.version.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;}
1821
.version.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;}
1822
.version.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;}
1823
.version.marker { background-image:url(/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1824

  
1825
.project.task_late { background:var(oc-red-5) url(/milestone_late.png); border: 1px solid var(oc-red-5); height: 2px; margin-top: 3px;}
1826
.project.task_done { background:var(--oc-green-7) url(/milestone_done.png); border: 1px solid var(--oc-green-7); height: 2px; margin-top: 3px;}
1827
.project.task_todo { background:var(--oc-white) url(/milestone_todo.png); border: 1px solid var(--oc-white); height: 2px; margin-top: 3px;}
1828
.project.marker { background-image:url(/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1829

  
1830
.version-behind-schedule a, .issue-behind-schedule a {color: var(--oc-yellow-8);}
1831
.version-overdue a, .issue-overdue a, .project-overdue a {color: var(--oc-red-8);}
1832 1706

  
1833 1707
/***** User events (ex: journal, notes, replies, comments) *****/
1834 1708
.journals h4.journal-header {
app/assets/stylesheets/gantt.css
1
/**
2
 * Redmine - project management software
3
 * Copyright (C) 2006-  Jean-Philippe Lang
4
 * This code is released under the GNU General Public License.
5
 */
6

  
7
.gantt_subjects div > span.expander {
8
  margin-left: 0;
9
  cursor: pointer;
10
}
11

  
12
.gantt_subjects .avatar {
13
  margin-right: 4px;
14
}
15

  
16
.gantt_subjects div.project-name a,
17
.gantt_subjects div.version-name a {
18
  margin-left: 4px;
19
}
20

  
21
/***** Gantt chart *****/
22
table.gantt-table {
23
  width: 100%;
24
  border-collapse: collapse;
25
}
26

  
27
table.gantt-table td {
28
  padding: 0;
29
}
30

  
31
.gantt_hdr {
32
  position: absolute;
33
  top: 0;
34
  height: 16px;
35
  border-top: 1px solid var(--oc-gray-4);
36
  border-bottom: 1px solid var(--oc-gray-4);
37
  border-left: 1px solid var(--oc-gray-4);
38
  text-align: center;
39
  overflow: hidden;
40
}
41

  
42
#gantt_area .gantt_hdr {
43
  border-left: 0;
44
  border-right: 1px solid var(--oc-gray-4);
45
}
46

  
47
.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr,
48
.last_gantt_selected_column .gantt_hdr {
49
  border-right: 1px solid var(--oc-gray-4);
50
}
51

  
52
.last_gantt_selected_column .gantt_selected_column_container,
53
.gantt_subjects_container .gantt_subjects * {
54
  z-index: 10;
55
}
56

  
57
.gantt_subjects_column + td {
58
  padding: 0;
59
}
60

  
61
.gantt_hdr.nwday {
62
  background-color: var(--oc-gray-1);
63
  color: var(--oc-gray-6);
64
}
65

  
66
.gantt_subjects,
67
.gantt_selected_column_content.gantt_hdr {
68
  font-size: 0.8em;
69
  position: relative;
70
  z-index: 1;
71
}
72

  
73
.gantt_subjects div,
74
.gantt_selected_column_content div {
75
  line-height: 16px;
76
  height: 16px;
77
  overflow: hidden;
78
  white-space: nowrap;
79
  text-overflow: clip;
80
  width: 100%;
81
}
82

  
83
.gantt_subjects div.issue-subject:hover {
84
  background-color: var(--oc-yellow-0);
85
}
86

  
87
.gantt_selected_column_content > div {
88
  padding-left: 3px;
89
  box-sizing: border-box;
90
}
91

  
92
.gantt_hdr_selected_column_name {
93
  position: absolute;
94
  top: 50%;
95
  width: 100%;
96
  transform: translateY(-50%);
97
  -webkit-transform: translateY(-50%);
98
  font-size: 0.8em;
99
  overflow: hidden;
100
  text-overflow: ellipsis;
101
  white-space: nowrap;
102
}
103

  
104
td.gantt_selected_column {
105
  width: 50px;
106
}
107

  
108
td.gantt_selected_column .gantt_hdr,
109
.gantt_selected_column_container {
110
  width: 49px;
111
}
112

  
113
td.gantt_watcher_users_column div.issue_watcher_users ul {
114
  margin: 0;
115
  padding: 0;
116
  list-style: none;
117
}
118

  
119
td.gantt_watcher_users_column div.issue_watcher_users ul li {
120
  display: inline;
121
}
122

  
123
td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
124
  content: ', ';
125
  white-space: pre;
126
}
127

  
128
.task {
129
  position: absolute;
130
  height: 8px;
131
  font-size: 0.8em;
132
  color: var(--oc-gray-6);
133
  padding: 0;
134
  margin: 0;
135
  line-height: 16px;
136
  white-space: nowrap;
137
}
138

  
139
.task.label {
140
  width: 100%;
141
}
142

  
143
.task.label.project,
144
.task.label.version {
145
  font-weight: bold;
146
}
147

  
148
.task_late {
149
  background: var(--oc-red-5) url(/task_late.png);
150
  border: 1px solid var(--oc-red-5);
151
}
152

  
153
.task_done {
154
  background: var(--oc-green-7) url(/task_done.png);
155
  border: 1px solid var(--oc-green-7);
156
}
157

  
158
.task_todo {
159
  background: var(--oc-gray-5) url(/task_todo.png);
160
  border: 1px solid var(--oc-gray-5);
161
}
162

  
163
.task_todo.parent {
164
  background: var(--oc-gray-6);
165
  border: 1px solid var(--oc-gray-6);
166
  height: 3px;
167
}
168

  
169
.task_late.parent,
170
.task_done.parent {
171
  height: 3px;
172
}
173

  
174
.task.parent.marker.starting {
175
  position: absolute;
176
  background: url(/task_parent_end.png) no-repeat 0 0;
177
  width: 8px;
178
  height: 16px;
179
  margin-left: -4px;
180
  left: 0;
181
  top: -1px;
182
}
183

  
184
.task.parent.marker.ending {
185
  position: absolute;
186
  background: url(/task_parent_end.png) no-repeat 0 0;
187
  width: 8px;
188
  height: 16px;
189
  margin-left: -4px;
190
  right: 0;
191
  top: -1px;
192
}
193

  
194
.version.task_late {
195
  background: var(oc-red-5) url(/milestone_late.png);
196
  border: 1px solid var(oc-red-5);
197
  height: 2px;
198
  margin-top: 3px;
199
}
200

  
201
.version.task_done {
202
  background: var(--oc-green-7) url(/milestone_done.png);
203
  border: 1px solid var(--oc-green-7);
204
  height: 2px;
205
  margin-top: 3px;
206
}
207

  
208
.version.task_todo {
209
  background: var(--oc-white) url(/milestone_todo.png);
210
  border: 1px solid var(--oc-white);
211
  height: 2px;
212
  margin-top: 3px;
213
}
214

  
215
.version.marker {
216
  background-image: url(/version_marker.png);
217
  background-repeat: no-repeat;
218
  border: 0;
219
  margin-left: -4px;
220
  margin-top: 1px;
221
}
222

  
223
.project.task_late {
224
  background: var(oc-red-5) url(/milestone_late.png);
225
  border: 1px solid var(oc-red-5);
226
  height: 2px;
227
  margin-top: 3px;
228
}
229

  
230
.project.task_done {
231
  background: var(--oc-green-7) url(/milestone_done.png);
232
  border: 1px solid var(--oc-green-7);
233
  height: 2px;
234
  margin-top: 3px;
235
}
236

  
237
.project.task_todo {
238
  background: var(--oc-white) url(/milestone_todo.png);
239
  border: 1px solid var(--oc-white);
240
  height: 2px;
241
  margin-top: 3px;
242
}
243

  
244
.project.marker {
245
  background-image: url(/project_marker.png);
246
  background-repeat: no-repeat;
247
  border: 0;
248
  margin-left: -4px;
249
  margin-top: 1px;
250
}
251

  
252
.version-behind-schedule a,
253
.issue-behind-schedule a {
254
  color: var(--oc-yellow-8);
255
}
256

  
257
.version-overdue a,
258
.issue-overdue a,
259
.project-overdue a {
260
  color: var(--oc-red-8);
261
}
app/helpers/gantt_helper.rb
41 41
      end
42 42
    end
43 43
  end
44

  
45
  def gantt_chart_tag(query, &)
46
    data_attributes = {
47
      controller: 'gantt--chart',
48
      # Events emitted by child controllers the chart listens to.
49
      # - `gantt--options` toggles checkboxes under Options.
50
      # - `gantt--subjects` reports tree expand/collapse.
51
      # - Window resize triggers a redraw of progress lines and relations.
52
      action: %w(
53
        gantt--options:toggle-display@document->gantt--chart#handleOptionsDisplay
54
        gantt--options:toggle-relations@document->gantt--chart#handleOptionsRelations
55
        gantt--options:toggle-progress@document->gantt--chart#handleOptionsProgress
56
        gantt--subjects:toggle-tree->gantt--chart#handleSubjectTreeChanged
57
        resize@window->gantt--chart#handleWindowResize
58
      ).join(' '),
59
      'gantt--chart-issue-relation-types-value': Redmine::Helpers::Gantt::DRAW_TYPES.to_json,
60
      'gantt--chart-show-selected-columns-value': query.draw_selected_columns ? 'true' : 'false',
61
      'gantt--chart-show-relations-value': query.draw_relations ? 'true' : 'false',
62
      'gantt--chart-show-progress-value': query.draw_progress_line ? 'true' : 'false'
63
    }
64

  
65
    tag.table(class: 'gantt-table', data: data_attributes, &)
66
  end
67

  
68
  def gantt_column_tag(column_name, min_width: nil, **options, &)
69
    options[:data] = {
70
      controller: 'gantt--column',
71
      action: 'resize@window->gantt--column#handleWindowResize',
72
      'gantt--column-min-width-value': min_width,
73
      'gantt--column-column-value': column_name
74
    }
75
    options[:class] = ["gantt_#{column_name}_column", options[:class]]
76

  
77
    tag.td(**options, &)
78
  end
79

  
80
  def gantt_subjects_tag(&)
81
    data_attributes = {
82
      controller: 'gantt--subjects',
83
      action: 'gantt--column:resize-column-subjects@document->gantt--subjects#handleResizeColumn'
84
    }
85
    tag.div(class: "gantt_subjects", data: data_attributes, &)
86
  end
44 87
end
app/javascript/controllers/gantt/chart_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
const RELATION_STROKE_WIDTH = 2
4

  
5
export default class extends Controller {
6
  static targets = ["ganttArea", "drawArea", "subjectsContainer"]
7

  
8
  static values = {
9
    issueRelationTypes: Object,
10
    showSelectedColumns: Boolean,
11
    showRelations: Boolean,
12
    showProgress: Boolean
13
  }
14

  
15
  #drawTop = 0
16
  #drawRight = 0
17
  #drawLeft = 0
18
  #drawPaper = null
19

  
20
  initialize() {
21
    this.$ = window.jQuery
22
    this.Raphael = window.Raphael
23
  }
24

  
25
  connect() {
26
    this.#drawTop = 0
27
    this.#drawRight = 0
28
    this.#drawLeft = 0
29

  
30
    this.#drawProgressLineAndRelations()
31
    this.#drawSelectedColumns()
32
  }
33

  
34
  disconnect() {
35
    if (this.#drawPaper) {
36
      this.#drawPaper.remove()
37
      this.#drawPaper = null
38
    }
39
  }
40

  
41
  showSelectedColumnsValueChanged() {
42
    this.#drawSelectedColumns()
43
  }
44

  
45
  showRelationsValueChanged() {
46
    this.#drawProgressLineAndRelations()
47
  }
48

  
49
  showProgressValueChanged() {
50
    this.#drawProgressLineAndRelations()
51
  }
52

  
53
  handleWindowResize() {
54
    this.#drawProgressLineAndRelations()
55
    this.#drawSelectedColumns()
56
  }
57

  
58
  handleSubjectTreeChanged() {
59
    this.#drawProgressLineAndRelations()
60
    this.#drawSelectedColumns()
61
  }
62

  
63
  handleOptionsDisplay(event) {
64
    this.showSelectedColumnsValue = !!(event.detail && event.detail.enabled)
65
  }
66

  
67
  handleOptionsRelations(event) {
68
    this.showRelationsValue = !!(event.detail && event.detail.enabled)
69
  }
70

  
71
  handleOptionsProgress(event) {
72
    this.showProgressValue = !!(event.detail && event.detail.enabled)
73
  }
74

  
75
  #drawProgressLineAndRelations() {
76
    if (this.#drawPaper) {
77
      this.#drawPaper.clear()
78
    } else {
79
      this.#drawPaper = this.Raphael(this.drawAreaTarget)
80
    }
81

  
82
    this.#setupDrawArea()
83

  
84
    if (this.showProgressValue) {
85
      this.#drawGanttProgressLines()
86
    }
87

  
88
    if (this.showRelationsValue) {
89
      this.#drawRelations()
90
    }
91

  
92
    const content = document.getElementById("content")
93
    if (content) {
94
      content.classList.add("gantt_content")
95
    }
96
  }
97

  
98
  #setupDrawArea() {
99
    const $drawArea = this.$(this.drawAreaTarget)
100
    const $ganttArea = this.hasGanttAreaTarget ? this.$(this.ganttAreaTarget) : null
101

  
102
    this.#drawTop = $drawArea.position().top
103
    this.#drawRight = $drawArea.width()
104
    this.#drawLeft = $ganttArea ? $ganttArea.scrollLeft() : 0
105
  }
106

  
107
  #drawSelectedColumns() {
108
    const $selectedColumns = this.$("td.gantt_selected_column")
109
    const $subjectsContainer = this.$(".gantt_subjects_container")
110

  
111
    const isMobileDevice = typeof window.isMobile === "function" && window.isMobile()
112

  
113
    if (this.showSelectedColumnsValue) {
114
      if (isMobileDevice) {
115
        $selectedColumns.each((_, element) => {
116
          this.$(element).hide()
117
        })
118
      } else {
119
        $subjectsContainer.addClass("draw_selected_columns")
120
        $selectedColumns.show()
121
      }
122
    } else {
123
      $selectedColumns.each((_, element) => {
124
        this.$(element).hide()
125
      })
126
      $subjectsContainer.removeClass("draw_selected_columns")
127
    }
128
  }
129

  
130
  get #relationsArray() {
131
    const relations = []
132

  
133
    this.$("div.task_todo[data-rels]").each((_, element) => {
134
      const $element = this.$(element)
135

  
136
      if (!$element.is(":visible")) return
137

  
138
      const elementId = $element.attr("id")
139

  
140
      if (!elementId) return
141

  
142
      const issueId = elementId.replace("task-todo-issue-", "")
143
      const dataRels = $element.data("rels") || {}
144

  
145
      Object.keys(dataRels).forEach((relTypeKey) => {
146
        this.$.each(dataRels[relTypeKey], (_, relatedIssue) => {
147
          relations.push({ issue_from: issueId, issue_to: relatedIssue, rel_type: relTypeKey })
148
        })
149
      })
150
    })
151

  
152
    return relations
153
  }
154

  
155
  #drawRelations() {
156
    const relations = this.#relationsArray
157

  
158
    relations.forEach((relation) => {
159
      const issueFrom = this.$(`#task-todo-issue-${relation.issue_from}`)
160
      const issueTo = this.$(`#task-todo-issue-${relation.issue_to}`)
161

  
162
      if (issueFrom.length === 0 || issueTo.length === 0) return
163

  
164
      const issueHeight = issueFrom.height()
165
      const issueFromTop = issueFrom.position().top + issueHeight / 2 - this.#drawTop
166
      const issueFromRight = issueFrom.position().left + issueFrom.width()
167
      const issueToTop = issueTo.position().top + issueHeight / 2 - this.#drawTop
168
      const issueToLeft = issueTo.position().left
169
      const relationConfig = this.issueRelationTypesValue[relation.rel_type] || {}
170
      const color = relationConfig.color || "#000"
171
      const landscapeMargin = relationConfig.landscape_margin || 0
172
      const issueFromRightRel = issueFromRight + landscapeMargin
173
      const issueToLeftRel = issueToLeft - landscapeMargin
174

  
175
      this.#drawPaper
176
        .path([
177
          "M",
178
          issueFromRight + this.#drawLeft,
179
          issueFromTop,
180
          "L",
181
          issueFromRightRel + this.#drawLeft,
182
          issueFromTop
183
        ])
184
        .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
185

  
186
      if (issueFromRightRel < issueToLeftRel) {
187
        this.#drawPaper
188
          .path([
189
            "M",
190
            issueFromRightRel + this.#drawLeft,
191
            issueFromTop,
192
            "L",
193
            issueFromRightRel + this.#drawLeft,
194
            issueToTop
195
          ])
196
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
197
        this.#drawPaper
198
          .path([
199
            "M",
200
            issueFromRightRel + this.#drawLeft,
201
            issueToTop,
202
            "L",
203
            issueToLeft + this.#drawLeft,
204
            issueToTop
205
          ])
206
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
207
      } else {
208
        const issueMiddleTop = issueToTop + issueHeight * (issueFromTop > issueToTop ? 1 : -1)
209
        this.#drawPaper
210
          .path([
211
            "M",
212
            issueFromRightRel + this.#drawLeft,
213
            issueFromTop,
214
            "L",
215
            issueFromRightRel + this.#drawLeft,
216
            issueMiddleTop
217
          ])
218
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
219
        this.#drawPaper
220
          .path([
221
            "M",
222
            issueFromRightRel + this.#drawLeft,
223
            issueMiddleTop,
224
            "L",
225
            issueToLeftRel + this.#drawLeft,
226
            issueMiddleTop
227
          ])
228
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
229
        this.#drawPaper
230
          .path([
231
            "M",
232
            issueToLeftRel + this.#drawLeft,
233
            issueMiddleTop,
234
            "L",
235
            issueToLeftRel + this.#drawLeft,
236
            issueToTop
237
          ])
238
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
239
        this.#drawPaper
240
          .path([
241
            "M",
242
            issueToLeftRel + this.#drawLeft,
243
            issueToTop,
244
            "L",
245
            issueToLeft + this.#drawLeft,
246
            issueToTop
247
          ])
248
          .attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
249
      }
250
      this.#drawPaper
251
        .path([
252
          "M",
253
          issueToLeft + this.#drawLeft,
254
          issueToTop,
255
          "l",
256
          -4 * RELATION_STROKE_WIDTH,
257
          -2 * RELATION_STROKE_WIDTH,
258
          "l",
259
          0,
260
          4 * RELATION_STROKE_WIDTH,
261
          "z"
262
        ])
263
        .attr({
264
          stroke: "none",
265
          fill: color,
266
          "stroke-linecap": "butt",
267
          "stroke-linejoin": "miter"
268
        })
269
    })
270
  }
271

  
272
  get #progressLinesArray() {
273
    const lines = []
274
    const todayLeft = this.$("#today_line").position().left
275

  
276
    lines.push({ left: todayLeft, top: 0 })
277

  
278
    this.$("div.issue-subject, div.version-name").each((_, element) => {
279
      const $element = this.$(element)
280

  
281
      if (!$element.is(":visible")) return true
282

  
283
      const topPosition = $element.position().top - this.#drawTop
284
      const elementHeight = $element.height() / 9
285
      const elementTopUpper = topPosition - elementHeight
286
      const elementTopCenter = topPosition + elementHeight * 3
287
      const elementTopLower = topPosition + elementHeight * 8
288
      const issueClosed = $element.children("span").hasClass("issue-closed")
289
      const versionClosed = $element.children("span").hasClass("version-closed")
290

  
291
      if (issueClosed || versionClosed) {
292
        lines.push({ left: todayLeft, top: elementTopCenter })
293
      } else {
294
        const issueDone = this.$(`#task-done-${$element.attr("id")}`)
295
        const isBehindStart = $element.children("span").hasClass("behind-start-date")
296
        const isOverEnd = $element.children("span").hasClass("over-end-date")
297

  
298
        if (isOverEnd) {
299
          lines.push({ left: this.#drawRight, top: elementTopUpper, is_right_edge: true })
300
          lines.push({
301
            left: this.#drawRight,
302
            top: elementTopLower,
303
            is_right_edge: true,
304
            none_stroke: true
305
          })
306
        } else if (issueDone.length > 0) {
307
          const doneLeft = issueDone.first().position().left + issueDone.first().width()
308
          lines.push({ left: doneLeft, top: elementTopCenter })
309
        } else if (isBehindStart) {
310
          lines.push({ left: 0, top: elementTopUpper, is_left_edge: true })
311
          lines.push({
312
            left: 0,
313
            top: elementTopLower,
314
            is_left_edge: true,
315
            none_stroke: true
316
          })
317
        } else {
318
          let todoLeft = todayLeft
319
          const issueTodo = this.$(`#task-todo-${$element.attr("id")}`)
320
          if (issueTodo.length > 0) {
321
            todoLeft = issueTodo.first().position().left
322
          }
323
          lines.push({ left: Math.min(todayLeft, todoLeft), top: elementTopCenter })
324
        }
325
      }
326
    })
327

  
328
    return lines
329
  }
330

  
331
  #drawGanttProgressLines() {
332
    if (this.$("#today_line").length === 0) return
333

  
334
    const progressLines = this.#progressLinesArray
335
    const color = this.$("#today_line").css("border-left-color") || "#ff0000"
336

  
337
    for (let index = 1; index < progressLines.length; index += 1) {
338
      const current = progressLines[index]
339
      const previous = progressLines[index - 1]
340

  
341
      if (
342
        !current.none_stroke &&
343
        !(
344
          (previous.is_right_edge && current.is_right_edge) ||
345
          (previous.is_left_edge && current.is_left_edge)
346
        )
347
      ) {
348
        const x1 = previous.left === 0 ? 0 : previous.left + this.#drawLeft
349
        const x2 = current.left === 0 ? 0 : current.left + this.#drawLeft
350

  
351
        this.#drawPaper
352
          .path(["M", x1, previous.top, "L", x2, current.top])
353
          .attr({ stroke: color, "stroke-width": 2 })
354
      }
355
    }
356
  }
357
}
app/javascript/controllers/gantt/column_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
export default class extends Controller {
4
  static values = {
5
    minWidth: Number,
6
    column: String,
7
    // Local value
8
    mobileMode: { type: Boolean, default: false }
9
  }
10

  
11
  #$element = null
12

  
13
  initialize() {
14
    this.$ = window.jQuery
15
  }
16

  
17
  connect() {
18
    this.#$element = this.$(this.element)
19
    this.#setupResizable()
20
    this.#dispatchResizeColumn()
21
  }
22

  
23
  disconnect() {
24
    this.#$element?.resizable("destroy")
25
    this.#$element = null
26
  }
27

  
28
  handleWindowResize(_event) {
29
    this.mobileModeValue = this.#isMobile()
30

  
31
    this.#dispatchResizeColumn()
32
  }
33

  
34
  mobileModeValueChanged(current, old) {
35
    if (current == old) return
36

  
37
    if (this.mobileModeValue) {
38
      this.#$element?.resizable("disable")
39
    } else {
40
      this.#$element?.resizable("enable")
41
    }
42
  }
43

  
44
  #setupResizable() {
45
    const alsoResize = [
46
      `.gantt_${this.columnValue}_container`,
47
      `.gantt_${this.columnValue}_container > .gantt_hdr`
48
    ]
49
    const options = {
50
      handles: "e",
51
      minWidth: this.minWidthValue,
52
      zIndex: 30,
53
      alsoResize: alsoResize.join(","),
54
      create: () => {
55
        this.$(".ui-resizable-e").css("cursor", "ew-resize")
56
      }
57
    }
58

  
59
    this.#$element
60
      .resizable(options)
61
      .on("resize", (event) => {
62
        event.stopPropagation()
63
        this.#dispatchResizeColumn()
64
      })
65
  }
66

  
67
  #dispatchResizeColumn() {
68
    if (!this.#$element) return
69

  
70
    this.dispatch(`resize-column-${this.columnValue}`, { detail: { width: this.#$element.width() } })
71
  }
72

  
73
  #isMobile() {
74
    return !!(typeof window.isMobile === "function" && window.isMobile())
75
  }
76
}
app/javascript/controllers/gantt/options_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
export default class extends Controller {
4
  static targets = ["display", "relations", "progress"]
5

  
6
  static values = {
7
    unavailableColumns: Array
8
  }
9

  
10
  initialize() {
11
    this.$ = window.jQuery
12
  }
13

  
14
  connect() {
15
    this.#dispatchInitialStates()
16
    this.#disableUnavailableColumns()
17
  }
18

  
19
  toggleDisplay(event) {
20
    this.dispatch("toggle-display", {
21
      detail: { enabled: event.currentTarget.checked }
22
    })
23
  }
24

  
25
  toggleRelations(event) {
26
    this.dispatch("toggle-relations", {
27
      detail: { enabled: event.currentTarget.checked }
28
    })
29
  }
30

  
31
  toggleProgress(event) {
32
    this.dispatch("toggle-progress", {
33
      detail: { enabled: event.currentTarget.checked }
34
    })
35
  }
36

  
37
  #dispatchInitialStates() {
38
    if (this.hasDisplayTarget) {
39
      this.dispatch("toggle-display", {
40
        detail: { enabled: this.displayTarget.checked }
41
      })
42
    }
43
    if (this.hasRelationsTarget) {
44
      this.dispatch("toggle-relations", {
45
        detail: { enabled: this.relationsTarget.checked }
46
      })
47
    }
48
    if (this.hasProgressTarget) {
49
      this.dispatch("toggle-progress", {
50
        detail: { enabled: this.progressTarget.checked }
51
      })
52
    }
53
  }
54

  
55
  #disableUnavailableColumns() {
56
    if (!Array.isArray(this.unavailableColumnsValue)) {
57
      return
58
    }
59
    this.unavailableColumnsValue.forEach((column) => {
60
      this.$("#available_c, #selected_c").children(`[value='${column}']`).prop("disabled", true)
61
    })
62
  }
63
}
app/javascript/controllers/gantt/subjects_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
export default class extends Controller {
4
  initialize() {
5
    this.$ = window.jQuery
6
  }
7

  
8
  handleResizeColumn(event) {
9
    const columnWidth = event.detail.width;
10

  
11
    this.$(".issue-subject, .project-name, .version-name").each((_, element) => {
12
      const $element = this.$(element)
13
      $element.width(columnWidth - $element.position().left)
14
    })
15
  }
16

  
17
  handleEntryClick(event) {
18
    const iconExpander = event.currentTarget
19
    const $subject = this.$(iconExpander.parentElement)
20
    const subjectLeft =
21
      parseInt($subject.css("left"), 10) + parseInt(iconExpander.offsetWidth, 10)
22

  
23
    let targetShown = null
24
    let targetTop = 0
25
    let totalHeight = 0
26
    let outOfHierarchy = false
27

  
28
    const willOpen = !$subject.hasClass("open")
29

  
30
    this.#setIconState($subject, willOpen)
31

  
32
    $subject.nextAll("div").each((_, element) => {
33
      const $element = this.$(element)
34
      const json = $element.data("collapse-expand")
35
      const numberOfRows = $element.data("number-of-rows")
36
      const barsSelector = `#gantt_area form > div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
37
      const selectedColumnsSelector = `td.gantt_selected_column div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
38

  
39
      if (outOfHierarchy || parseInt($element.css("left"), 10) <= subjectLeft) {
40
        outOfHierarchy = true
41

  
42
        if (targetShown === null) return false
43

  
44
        const newTopVal = parseInt($element.css("top"), 10) + totalHeight * (targetShown ? -1 : 1)
45

  
46
        $element.css("top", newTopVal)
47
        this.$([barsSelector, selectedColumnsSelector].join()).each((__, el) => {
48
          this.$(el).css("top", newTopVal)
49
        })
50

  
51
        return true
52
      }
53

  
54
      const isShown = $element.is(":visible")
55

  
56
      if (targetShown === null) {
57
        targetShown = isShown
58
        targetTop = parseInt($element.css("top"), 10)
59
        totalHeight = 0
60
      }
61

  
62
      if (isShown === targetShown) {
63
        this.$(barsSelector).each((__, task) => {
64
          const $task = this.$(task)
65

  
66
          if (!isShown && willOpen) {
67
            $task.css("top", targetTop + totalHeight)
68
          }
69
          if (!$task.hasClass("tooltip")) {
70
            $task.toggle(willOpen)
71
          }
72
        })
73

  
74
        this.$(selectedColumnsSelector).each((__, attr) => {
75
          const $attr = this.$(attr)
76

  
77
          if (!isShown && willOpen) {
78
            $attr.css("top", targetTop + totalHeight)
79
          }
80
          $attr.toggle(willOpen)
81
        })
82

  
83
        if (!isShown && willOpen) {
84
          $element.css("top", targetTop + totalHeight)
85
        }
86

  
87
        this.#setIconState($element, willOpen)
88
        $element.toggle(willOpen)
89
        totalHeight += parseInt(json.top_increment, 10)
90
      }
91
    })
92

  
93
    this.dispatch("toggle-tree", { bubbles: true })
94
  }
95

  
96
  #setIconState(element, open) {
97
    const $element = element.jquery ? element : this.$(element)
98
    const expander = $element.find(".expander")
99

  
100
    if (open) {
101
      $element.addClass("open")
102

  
103
      if (expander.length > 0) {
104
        expander.removeClass("icon-collapsed").addClass("icon-expanded")
105

  
106
        if (expander.find("svg").length === 1) {
107
          window.updateSVGIcon(expander[0], "angle-down")
108
        }
109
      }
110
    } else {
111
      $element.removeClass("open")
112

  
113
      if (expander.length > 0) {
114
        expander.removeClass("icon-expanded").addClass("icon-collapsed")
115

  
116
        if (expander.find("svg").length === 1) {
117
          window.updateSVGIcon(expander[0], "angle-right")
118
        }
119
      }
120
    }
121
  }
122
}
app/views/gantts/_chart.html.erb
1
<%
2
  zoom = 1
3
  gantt.zoom.times { zoom *= 2 }
4

  
5
  subject_width = 330
6
  header_height = 18
7

  
8
  headers_height = header_height
9
  show_weeks = false
10
  show_days = false
11
  show_day_num = false
12

  
13
  if gantt.zoom > 1
14
    show_weeks = true
15
    headers_height = 2 * header_height
16
    if gantt.zoom > 2
17
      show_days = true
18
      headers_height = 3 * header_height
19
      if gantt.zoom > 3
20
        show_day_num = true
21
        headers_height = 4 * header_height
22
      end
23
    end
24
  end
25

  
26
  g_width = ((gantt.date_to - gantt.date_from + 1) * zoom).to_i
27
  gantt.render(
28
    top: headers_height + 8,
29
    zoom: zoom,
30
    g_width: g_width,
31
    subject_width: subject_width
32
  )
33
  g_height = [(20 * (gantt.number_of_rows + 6)) + 150, 206].max
34
  t_height = g_height + headers_height
... This diff was truncated because it exceeds the maximum size that can be displayed.
(3-3/5)