Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

419 řádky
12 KiB

  1. import date_utils from './date_utils';
  2. import { $, createSVG, animateSVG } from './svg_utils';
  3. export default class Bar {
  4. constructor(gantt, task) {
  5. this.set_defaults(gantt, task);
  6. this.prepare();
  7. this.draw();
  8. this.bind();
  9. }
  10. set_defaults(gantt, task) {
  11. this.action_completed = false;
  12. this.gantt = gantt;
  13. this.task = task;
  14. }
  15. prepare() {
  16. this.prepare_values();
  17. this.prepare_helpers();
  18. }
  19. prepare_values() {
  20. this.invalid = this.task.invalid;
  21. this.height = this.gantt.options.bar_height;
  22. this.x = this.compute_x();
  23. this.y = this.compute_y();
  24. this.corner_radius = this.gantt.options.bar_corner_radius;
  25. this.duration =
  26. date_utils.diff(this.task._end, this.task._start, 'hour') /
  27. this.gantt.options.step;
  28. this.width = this.gantt.options.column_width * this.duration;
  29. this.progress_width =
  30. this.gantt.options.column_width *
  31. this.duration *
  32. (this.task.progress / 100) || 0;
  33. this.group = createSVG('g', {
  34. class: 'bar-wrapper ' + (this.task.custom_class || ''),
  35. 'data-id': this.task.id,
  36. });
  37. this.bar_group = createSVG('g', {
  38. class: 'bar-group',
  39. append_to: this.group,
  40. });
  41. this.handle_group = createSVG('g', {
  42. class: 'handle-group',
  43. append_to: this.group,
  44. });
  45. }
  46. prepare_helpers() {
  47. SVGElement.prototype.getX = function () {
  48. return +this.getAttribute('x');
  49. };
  50. SVGElement.prototype.getY = function () {
  51. return +this.getAttribute('y');
  52. };
  53. SVGElement.prototype.getWidth = function () {
  54. return +this.getAttribute('width');
  55. };
  56. SVGElement.prototype.getHeight = function () {
  57. return +this.getAttribute('height');
  58. };
  59. SVGElement.prototype.getEndX = function () {
  60. return this.getX() + this.getWidth();
  61. };
  62. }
  63. draw() {
  64. this.draw_bar();
  65. this.draw_progress_bar();
  66. this.draw_label();
  67. this.draw_resize_handles();
  68. }
  69. draw_bar() {
  70. this.$bar = createSVG('rect', {
  71. x: this.x,
  72. y: this.y,
  73. width: this.width,
  74. height: this.height,
  75. rx: this.corner_radius,
  76. ry: this.corner_radius,
  77. class: 'bar',
  78. append_to: this.bar_group,
  79. });
  80. animateSVG(this.$bar, 'width', 0, this.width);
  81. if (this.invalid) {
  82. this.$bar.classList.add('bar-invalid');
  83. }
  84. }
  85. draw_progress_bar() {
  86. if (this.invalid) return;
  87. this.$bar_progress = createSVG('rect', {
  88. x: this.x,
  89. y: this.y,
  90. width: this.progress_width,
  91. height: this.height,
  92. rx: this.corner_radius,
  93. ry: this.corner_radius,
  94. class: 'bar-progress',
  95. append_to: this.bar_group,
  96. });
  97. animateSVG(this.$bar_progress, 'width', 0, this.progress_width);
  98. }
  99. draw_label() {
  100. createSVG('text', {
  101. x: this.x + this.width / 2,
  102. y: this.y + this.height / 2,
  103. innerHTML: this.task.name,
  104. class: 'bar-label',
  105. append_to: this.bar_group,
  106. });
  107. // labels get BBox in the next tick
  108. requestAnimationFrame(() => this.update_label_position());
  109. }
  110. draw_resize_handles() {
  111. if (this.invalid) return;
  112. const bar = this.$bar;
  113. const handle_width = 8;
  114. createSVG('rect', {
  115. x: bar.getX() + bar.getWidth() - 9,
  116. y: bar.getY() + 1,
  117. width: handle_width,
  118. height: this.height - 2,
  119. rx: this.corner_radius,
  120. ry: this.corner_radius,
  121. class: 'handle right',
  122. append_to: this.handle_group,
  123. });
  124. createSVG('rect', {
  125. x: bar.getX() + 1,
  126. y: bar.getY() + 1,
  127. width: handle_width,
  128. height: this.height - 2,
  129. rx: this.corner_radius,
  130. ry: this.corner_radius,
  131. class: 'handle left',
  132. append_to: this.handle_group,
  133. });
  134. if (this.task.progress && this.task.progress < 100) {
  135. this.$handle_progress = createSVG('polygon', {
  136. points: this.get_progress_polygon_points().join(','),
  137. class: 'handle progress',
  138. append_to: this.handle_group,
  139. });
  140. }
  141. }
  142. get_progress_polygon_points() {
  143. const bar_progress = this.$bar_progress;
  144. return [
  145. bar_progress.getEndX() - 5,
  146. bar_progress.getY() + bar_progress.getHeight(),
  147. bar_progress.getEndX() + 5,
  148. bar_progress.getY() + bar_progress.getHeight(),
  149. bar_progress.getEndX(),
  150. bar_progress.getY() + bar_progress.getHeight() - 8.66,
  151. ];
  152. }
  153. bind() {
  154. if (this.invalid) return;
  155. this.setup_click_event();
  156. }
  157. setup_click_event() {
  158. $.on(this.group, 'focus ' + this.gantt.options.popup_trigger, (e) => {
  159. if (this.action_completed) {
  160. // just finished a move action, wait for a few seconds
  161. return;
  162. }
  163. this.show_popup();
  164. this.gantt.unselect_all();
  165. this.group.classList.add('active');
  166. });
  167. $.on(this.group, 'dblclick', (e) => {
  168. if (this.action_completed) {
  169. // just finished a move action, wait for a few seconds
  170. return;
  171. }
  172. this.gantt.trigger_event('click', [this.task]);
  173. });
  174. }
  175. show_popup() {
  176. if (this.gantt.bar_being_dragged) return;
  177. const start_date = date_utils.format(
  178. this.task._start,
  179. 'MMM D',
  180. this.gantt.options.language
  181. );
  182. const end_date = date_utils.format(
  183. date_utils.add(this.task._end, -1, 'second'),
  184. 'MMM D',
  185. this.gantt.options.language
  186. );
  187. const subtitle = start_date + ' - ' + end_date;
  188. this.gantt.show_popup({
  189. target_element: this.$bar,
  190. title: this.task.name,
  191. subtitle: subtitle,
  192. task: this.task,
  193. });
  194. }
  195. update_bar_position({ x = null, width = null }) {
  196. const bar = this.$bar;
  197. if (x) {
  198. // get all x values of parent task
  199. const xs = this.task.dependencies.map((dep) => {
  200. return this.gantt.get_bar(dep).$bar.getX();
  201. });
  202. // child task must not go before parent
  203. const valid_x = xs.reduce((prev, curr) => {
  204. return x >= curr;
  205. }, x);
  206. if (!valid_x) {
  207. width = null;
  208. return;
  209. }
  210. this.update_attr(bar, 'x', x);
  211. }
  212. if (width && width >= this.gantt.options.column_width) {
  213. this.update_attr(bar, 'width', width);
  214. }
  215. this.update_label_position();
  216. this.update_handle_position();
  217. this.update_progressbar_position();
  218. this.update_arrow_position();
  219. }
  220. date_changed() {
  221. let changed = false;
  222. const { new_start_date, new_end_date } = this.compute_start_end_date();
  223. if (Number(this.task._start) !== Number(new_start_date)) {
  224. changed = true;
  225. this.task._start = new_start_date;
  226. }
  227. if (Number(this.task._end) !== Number(new_end_date)) {
  228. changed = true;
  229. this.task._end = new_end_date;
  230. }
  231. if (!changed) return;
  232. this.gantt.trigger_event('date_change', [
  233. this.task,
  234. new_start_date,
  235. date_utils.add(new_end_date, -1, 'second'),
  236. ]);
  237. }
  238. progress_changed() {
  239. const new_progress = this.compute_progress();
  240. this.task.progress = new_progress;
  241. this.gantt.trigger_event('progress_change', [this.task, new_progress]);
  242. }
  243. set_action_completed() {
  244. this.action_completed = true;
  245. setTimeout(() => (this.action_completed = false), 1000);
  246. }
  247. compute_start_end_date() {
  248. const bar = this.$bar;
  249. const x_in_units = bar.getX() / this.gantt.options.column_width;
  250. const new_start_date = date_utils.add(
  251. this.gantt.gantt_start,
  252. x_in_units * this.gantt.options.step,
  253. 'hour'
  254. );
  255. const width_in_units = bar.getWidth() / this.gantt.options.column_width;
  256. const new_end_date = date_utils.add(
  257. new_start_date,
  258. width_in_units * this.gantt.options.step,
  259. 'hour'
  260. );
  261. return { new_start_date, new_end_date };
  262. }
  263. compute_progress() {
  264. const progress =
  265. (this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100;
  266. return parseInt(progress, 10);
  267. }
  268. compute_x() {
  269. const { step, column_width } = this.gantt.options;
  270. const task_start = this.task._start;
  271. const gantt_start = this.gantt.gantt_start;
  272. const diff = date_utils.diff(task_start, gantt_start, 'hour');
  273. let x = (diff / step) * column_width;
  274. if (this.gantt.view_is('Month')) {
  275. const diff = date_utils.diff(task_start, gantt_start, 'day');
  276. x = (diff * column_width) / 30;
  277. }
  278. return x;
  279. }
  280. compute_y() {
  281. return (
  282. this.gantt.options.header_height +
  283. this.gantt.options.padding +
  284. this.task._index * (this.height + this.gantt.options.padding)
  285. );
  286. }
  287. get_snap_position(dx) {
  288. let odx = dx,
  289. rem,
  290. position;
  291. if (this.gantt.view_is('Week')) {
  292. rem = dx % (this.gantt.options.column_width / 7);
  293. position =
  294. odx -
  295. rem +
  296. (rem < this.gantt.options.column_width / 14
  297. ? 0
  298. : this.gantt.options.column_width / 7);
  299. } else if (this.gantt.view_is('Month')) {
  300. rem = dx % (this.gantt.options.column_width / 30);
  301. position =
  302. odx -
  303. rem +
  304. (rem < this.gantt.options.column_width / 60
  305. ? 0
  306. : this.gantt.options.column_width / 30);
  307. } else {
  308. rem = dx % this.gantt.options.column_width;
  309. position =
  310. odx -
  311. rem +
  312. (rem < this.gantt.options.column_width / 2
  313. ? 0
  314. : this.gantt.options.column_width);
  315. }
  316. return position;
  317. }
  318. update_attr(element, attr, value) {
  319. value = +value;
  320. if (!isNaN(value)) {
  321. element.setAttribute(attr, value);
  322. }
  323. return element;
  324. }
  325. update_progressbar_position() {
  326. this.$bar_progress.setAttribute('x', this.$bar.getX());
  327. this.$bar_progress.setAttribute(
  328. 'width',
  329. this.$bar.getWidth() * (this.task.progress / 100)
  330. );
  331. }
  332. update_label_position() {
  333. const bar = this.$bar,
  334. label = this.group.querySelector('.bar-label');
  335. if (label.getBBox().width > bar.getWidth()) {
  336. label.classList.add('big');
  337. label.setAttribute('x', bar.getX() + bar.getWidth() + 5);
  338. } else {
  339. label.classList.remove('big');
  340. label.setAttribute('x', bar.getX() + bar.getWidth() / 2);
  341. }
  342. }
  343. update_handle_position() {
  344. const bar = this.$bar;
  345. this.handle_group
  346. .querySelector('.handle.left')
  347. .setAttribute('x', bar.getX() + 1);
  348. this.handle_group
  349. .querySelector('.handle.right')
  350. .setAttribute('x', bar.getEndX() - 9);
  351. const handle = this.group.querySelector('.handle.progress');
  352. handle &&
  353. handle.setAttribute('points', this.get_progress_polygon_points());
  354. }
  355. update_arrow_position() {
  356. this.arrows = this.arrows || [];
  357. for (let arrow of this.arrows) {
  358. arrow.update();
  359. }
  360. }
  361. }
  362. function isFunction(functionToCheck) {
  363. var getType = {};
  364. return (
  365. functionToCheck &&
  366. getType.toString.call(functionToCheck) === '[object Function]'
  367. );
  368. }