You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gantt.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /* Redmine - project management software
  2. Copyright (C) 2006-2021 Jean-Philippe Lang */
  3. var draw_gantt = null;
  4. var draw_top;
  5. var draw_right;
  6. var draw_left;
  7. var rels_stroke_width = 2;
  8. function setDrawArea() {
  9. draw_top = $("#gantt_draw_area").position().top;
  10. draw_right = $("#gantt_draw_area").width();
  11. draw_left = $("#gantt_area").scrollLeft();
  12. }
  13. function getRelationsArray() {
  14. var arr = new Array();
  15. $.each($('div.task_todo[data-rels]'), function(index_div, element) {
  16. if(!$(element).is(':visible')) return true;
  17. var element_id = $(element).attr("id");
  18. if (element_id != null) {
  19. var issue_id = element_id.replace("task-todo-issue-", "");
  20. var data_rels = $(element).data("rels");
  21. for (rel_type_key in data_rels) {
  22. $.each(data_rels[rel_type_key], function(index_issue, element_issue) {
  23. arr.push({issue_from: issue_id, issue_to: element_issue,
  24. rel_type: rel_type_key});
  25. });
  26. }
  27. }
  28. });
  29. return arr;
  30. }
  31. function drawRelations() {
  32. var arr = getRelationsArray();
  33. $.each(arr, function(index_issue, element_issue) {
  34. var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
  35. var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
  36. if (issue_from.length == 0 || issue_to.length == 0) {
  37. return;
  38. }
  39. var issue_height = issue_from.height();
  40. var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top;
  41. var issue_from_right = issue_from.position().left + issue_from.width();
  42. var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top;
  43. var issue_to_left = issue_to.position().left;
  44. var color = issue_relation_type[element_issue["rel_type"]]["color"];
  45. var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
  46. var issue_from_right_rel = issue_from_right + landscape_margin;
  47. var issue_to_left_rel = issue_to_left - landscape_margin;
  48. draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top,
  49. "L", issue_from_right_rel + draw_left, issue_from_top])
  50. .attr({stroke: color,
  51. "stroke-width": rels_stroke_width
  52. });
  53. if (issue_from_right_rel < issue_to_left_rel) {
  54. draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
  55. "L", issue_from_right_rel + draw_left, issue_to_top])
  56. .attr({stroke: color,
  57. "stroke-width": rels_stroke_width
  58. });
  59. draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
  60. "L", issue_to_left + draw_left, issue_to_top])
  61. .attr({stroke: color,
  62. "stroke-width": rels_stroke_width
  63. });
  64. } else {
  65. var issue_middle_top = issue_to_top +
  66. (issue_height *
  67. ((issue_from_top > issue_to_top) ? 1 : -1));
  68. draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
  69. "L", issue_from_right_rel + draw_left, issue_middle_top])
  70. .attr({stroke: color,
  71. "stroke-width": rels_stroke_width
  72. });
  73. draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
  74. "L", issue_to_left_rel + draw_left, issue_middle_top])
  75. .attr({stroke: color,
  76. "stroke-width": rels_stroke_width
  77. });
  78. draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
  79. "L", issue_to_left_rel + draw_left, issue_to_top])
  80. .attr({stroke: color,
  81. "stroke-width": rels_stroke_width
  82. });
  83. draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
  84. "L", issue_to_left + draw_left, issue_to_top])
  85. .attr({stroke: color,
  86. "stroke-width": rels_stroke_width
  87. });
  88. }
  89. draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
  90. "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
  91. "l", 0, 4 * rels_stroke_width, "z"])
  92. .attr({stroke: "none",
  93. fill: color,
  94. "stroke-linecap": "butt",
  95. "stroke-linejoin": "miter"
  96. });
  97. });
  98. }
  99. function getProgressLinesArray() {
  100. var arr = new Array();
  101. var today_left = $('#today_line').position().left;
  102. arr.push({left: today_left, top: 0});
  103. $.each($('div.issue-subject, div.version-name'), function(index, element) {
  104. if(!$(element).is(':visible')) return true;
  105. var t = $(element).position().top - draw_top ;
  106. var h = ($(element).height() / 9);
  107. var element_top_upper = t - h;
  108. var element_top_center = t + (h * 3);
  109. var element_top_lower = t + (h * 8);
  110. var issue_closed = $(element).children('span').hasClass('issue-closed');
  111. var version_closed = $(element).children('span').hasClass('version-closed');
  112. if (issue_closed || version_closed) {
  113. arr.push({left: today_left, top: element_top_center});
  114. } else {
  115. var issue_done = $("#task-done-" + $(element).attr("id"));
  116. var is_behind_start = $(element).children('span').hasClass('behind-start-date');
  117. var is_over_end = $(element).children('span').hasClass('over-end-date');
  118. if (is_over_end) {
  119. arr.push({left: draw_right, top: element_top_upper, is_right_edge: true});
  120. arr.push({left: draw_right, top: element_top_lower, is_right_edge: true, none_stroke: true});
  121. } else if (issue_done.length > 0) {
  122. var done_left = issue_done.first().position().left +
  123. issue_done.first().width();
  124. arr.push({left: done_left, top: element_top_center});
  125. } else if (is_behind_start) {
  126. arr.push({left: 0 , top: element_top_upper, is_left_edge: true});
  127. arr.push({left: 0 , top: element_top_lower, is_left_edge: true, none_stroke: true});
  128. } else {
  129. var todo_left = today_left;
  130. var issue_todo = $("#task-todo-" + $(element).attr("id"));
  131. if (issue_todo.length > 0){
  132. todo_left = issue_todo.first().position().left;
  133. }
  134. arr.push({left: Math.min(today_left, todo_left), top: element_top_center});
  135. }
  136. }
  137. });
  138. return arr;
  139. }
  140. function drawGanttProgressLines() {
  141. var arr = getProgressLinesArray();
  142. var color = $("#today_line")
  143. .css("border-left-color");
  144. var i;
  145. for(i = 1 ; i < arr.length ; i++) {
  146. if (!("none_stroke" in arr[i]) &&
  147. (!("is_right_edge" in arr[i - 1] && "is_right_edge" in arr[i]) &&
  148. !("is_left_edge" in arr[i - 1] && "is_left_edge" in arr[i]))
  149. ) {
  150. var x1 = (arr[i - 1].left == 0) ? 0 : arr[i - 1].left + draw_left;
  151. var x2 = (arr[i].left == 0) ? 0 : arr[i].left + draw_left;
  152. draw_gantt.path(["M", x1, arr[i - 1].top,
  153. "L", x2, arr[i].top])
  154. .attr({stroke: color, "stroke-width": 2});
  155. }
  156. }
  157. }
  158. function drawSelectedColumns(){
  159. if ($("#draw_selected_columns").prop('checked')) {
  160. if(isMobile()) {
  161. $('td.gantt_selected_column').each(function(i) {
  162. $(this).hide();
  163. });
  164. }else{
  165. $('.gantt_subjects_container').addClass('draw_selected_columns');
  166. $('td.gantt_selected_column').each(function() {
  167. $(this).show();
  168. var column_name = $(this).attr('id');
  169. $(this).resizable({
  170. alsoResize: '.gantt_' + column_name + '_container, .gantt_' + column_name + '_container > .gantt_hdr',
  171. minWidth: 20,
  172. handles: "e",
  173. create: function() {
  174. $(".ui-resizable-e").css("cursor","ew-resize");
  175. }
  176. }).on('resize', function (e) {
  177. e.stopPropagation();
  178. });
  179. });
  180. }
  181. }else{
  182. $('td.gantt_selected_column').each(function (i) {
  183. $(this).hide();
  184. $('.gantt_subjects_container').removeClass('draw_selected_columns');
  185. });
  186. }
  187. }
  188. function drawGanttHandler() {
  189. var folder = document.getElementById('gantt_draw_area');
  190. if(draw_gantt != null)
  191. draw_gantt.clear();
  192. else
  193. draw_gantt = Raphael(folder);
  194. setDrawArea();
  195. drawSelectedColumns();
  196. if ($("#draw_progress_line").prop('checked'))
  197. try{drawGanttProgressLines();}catch(e){}
  198. if ($("#draw_relations").prop('checked'))
  199. drawRelations();
  200. $('#content').addClass('gantt_content');
  201. }
  202. function resizableSubjectColumn(){
  203. $('.issue-subject, .project-name, .version-name').each(function(){
  204. $(this).width($(".gantt_subjects_column").width()-$(this).position().left);
  205. });
  206. $('td.gantt_subjects_column').resizable({
  207. alsoResize: '.gantt_subjects_container, .gantt_subjects_container>.gantt_hdr, .project-name, .issue-subject, .version-name',
  208. minWidth: 100,
  209. handles: 'e',
  210. create: function( event, ui ) {
  211. $('.ui-resizable-e').css('cursor','ew-resize');
  212. }
  213. }).on('resize', function (e) {
  214. e.stopPropagation();
  215. });
  216. if(isMobile()) {
  217. $('td.gantt_subjects_column').resizable('disable');
  218. }else{
  219. $('td.gantt_subjects_column').resizable('enable');
  220. };
  221. }
  222. ganttEntryClick = function(e){
  223. var icon_expander = e.target;
  224. var subject = $(icon_expander.parentElement);
  225. var subject_left = parseInt(subject.css('left')) + parseInt(icon_expander.offsetWidth);
  226. var target_shown = null;
  227. var target_top = 0;
  228. var total_height = 0;
  229. var out_of_hierarchy = false;
  230. var iconChange = null;
  231. if(subject.hasClass('open'))
  232. iconChange = function(element){
  233. $(element).find('.expander').switchClass('icon-expended', 'icon-collapsed');
  234. $(element).removeClass('open');
  235. };
  236. else
  237. iconChange = function(element){
  238. $(element).find('.expander').switchClass('icon-collapsed', 'icon-expended');
  239. $(element).addClass('open');
  240. };
  241. iconChange(subject);
  242. subject.nextAll('div').each(function(_, element){
  243. var el = $(element);
  244. var json = el.data('collapse-expand');
  245. if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
  246. out_of_hierarchy = true;
  247. if(target_shown == null) return false;
  248. var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
  249. el.css('top', new_top_val);
  250. $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"], td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, el){
  251. $(el).css('top', new_top_val);
  252. });
  253. return true;
  254. }
  255. var is_shown = el.is(':visible');
  256. if(target_shown == null){
  257. target_shown = is_shown;
  258. target_top = parseInt(el.css('top'));
  259. total_height = 0;
  260. }
  261. if(is_shown == target_shown){
  262. $('#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"]').each(function(_, task) {
  263. var el_task = $(task);
  264. if(!is_shown)
  265. el_task.css('top', target_top + total_height);
  266. if(!el_task.hasClass('tooltip'))
  267. el_task.toggle(!is_shown);
  268. });
  269. $('td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"]'
  270. ).each(function (_, attr) {
  271. var el_attr = $(attr);
  272. if (!is_shown)
  273. el_attr.css('top', target_top + total_height);
  274. el_attr.toggle(!is_shown);
  275. });
  276. if(!is_shown)
  277. el.css('top', target_top + total_height);
  278. iconChange(el);
  279. el.toggle(!is_shown);
  280. total_height += parseInt(json.top_increment);
  281. }
  282. });
  283. drawGanttHandler();
  284. };
  285. function disable_unavailable_columns(unavailable_columns) {
  286. $.each(unavailable_columns, function (index, value) {
  287. $('#available_c, #selected_c').children("[value='" + value + "']").prop('disabled', true);
  288. });
  289. }