25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1968 lines
64 KiB

  1. var Gantt = (function () {
  2. 'use strict';
  3. const YEAR = 'year';
  4. const MONTH = 'month';
  5. const DAY = 'day';
  6. const HOUR = 'hour';
  7. const MINUTE = 'minute';
  8. const SECOND = 'second';
  9. const MILLISECOND = 'millisecond';
  10. const month_names = {
  11. en: [
  12. 'January',
  13. 'February',
  14. 'March',
  15. 'April',
  16. 'May',
  17. 'June',
  18. 'July',
  19. 'August',
  20. 'September',
  21. 'October',
  22. 'November',
  23. 'December',
  24. ],
  25. es: [
  26. 'Enero',
  27. 'Febrero',
  28. 'Marzo',
  29. 'Abril',
  30. 'Mayo',
  31. 'Junio',
  32. 'Julio',
  33. 'Agosto',
  34. 'Septiembre',
  35. 'Octubre',
  36. 'Noviembre',
  37. 'Diciembre',
  38. ],
  39. ru: [
  40. 'Январь',
  41. 'Февраль',
  42. 'Март',
  43. 'Апрель',
  44. 'Май',
  45. 'Июнь',
  46. 'Июль',
  47. 'Август',
  48. 'Сентябрь',
  49. 'Октябрь',
  50. 'Ноябрь',
  51. 'Декабрь',
  52. ],
  53. ptBr: [
  54. 'Janeiro',
  55. 'Fevereiro',
  56. 'Março',
  57. 'Abril',
  58. 'Maio',
  59. 'Junho',
  60. 'Julho',
  61. 'Agosto',
  62. 'Setembro',
  63. 'Outubro',
  64. 'Novembro',
  65. 'Dezembro',
  66. ],
  67. fr: [
  68. 'Janvier',
  69. 'Février',
  70. 'Mars',
  71. 'Avril',
  72. 'Mai',
  73. 'Juin',
  74. 'Juillet',
  75. 'Août',
  76. 'Septembre',
  77. 'Octobre',
  78. 'Novembre',
  79. 'Décembre',
  80. ],
  81. tr: [
  82. 'Ocak',
  83. 'Şubat',
  84. 'Mart',
  85. 'Nisan',
  86. 'Mayıs',
  87. 'Haziran',
  88. 'Temmuz',
  89. 'Ağustos',
  90. 'Eylül',
  91. 'Ekim',
  92. 'Kasım',
  93. 'Aralık',
  94. ],
  95. zh: [
  96. '一月',
  97. '二月',
  98. '三月',
  99. '四月',
  100. '五月',
  101. '六月',
  102. '七月',
  103. '八月',
  104. '九月',
  105. '十月',
  106. '十一月',
  107. '十二月',
  108. ],
  109. };
  110. var date_utils = {
  111. parse(date, date_separator = '-', time_separator = /[.:]/) {
  112. if (date instanceof Date) {
  113. return date;
  114. }
  115. if (typeof date === 'string') {
  116. let date_parts, time_parts;
  117. const parts = date.split(' ');
  118. date_parts = parts[0]
  119. .split(date_separator)
  120. .map((val) => parseInt(val, 10));
  121. time_parts = parts[1] && parts[1].split(time_separator);
  122. // month is 0 indexed
  123. date_parts[1] = date_parts[1] - 1;
  124. let vals = date_parts;
  125. if (time_parts && time_parts.length) {
  126. if (time_parts.length == 4) {
  127. time_parts[3] = '0.' + time_parts[3];
  128. time_parts[3] = parseFloat(time_parts[3]) * 1000;
  129. }
  130. vals = vals.concat(time_parts);
  131. }
  132. return new Date(...vals);
  133. }
  134. },
  135. to_string(date, with_time = false) {
  136. if (!(date instanceof Date)) {
  137. throw new TypeError('Invalid argument type');
  138. }
  139. const vals = this.get_date_values(date).map((val, i) => {
  140. if (i === 1) {
  141. // add 1 for month
  142. val = val + 1;
  143. }
  144. if (i === 6) {
  145. return padStart(val + '', 3, '0');
  146. }
  147. return padStart(val + '', 2, '0');
  148. });
  149. const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`;
  150. const time_string = `${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}`;
  151. return date_string + (with_time ? ' ' + time_string : '');
  152. },
  153. format(date, format_string = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') {
  154. const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));
  155. const format_map = {
  156. YYYY: values[0],
  157. MM: padStart(+values[1] + 1, 2, 0),
  158. DD: values[2],
  159. HH: values[3],
  160. mm: values[4],
  161. ss: values[5],
  162. SSS: values[6],
  163. D: values[2],
  164. MMMM: month_names[lang][+values[1]],
  165. MMM: month_names[lang][+values[1]],
  166. };
  167. let str = format_string;
  168. const formatted_values = [];
  169. Object.keys(format_map)
  170. .sort((a, b) => b.length - a.length) // big string first
  171. .forEach((key) => {
  172. if (str.includes(key)) {
  173. str = str.replace(key, `$${formatted_values.length}`);
  174. formatted_values.push(format_map[key]);
  175. }
  176. });
  177. formatted_values.forEach((value, i) => {
  178. str = str.replace(`$${i}`, value);
  179. });
  180. return str;
  181. },
  182. diff(date_a, date_b, scale = DAY) {
  183. let milliseconds, seconds, hours, minutes, days, months, years;
  184. milliseconds = date_a - date_b;
  185. seconds = milliseconds / 1000;
  186. minutes = seconds / 60;
  187. hours = minutes / 60;
  188. days = hours / 24;
  189. months = days / 30;
  190. years = months / 12;
  191. if (!scale.endsWith('s')) {
  192. scale += 's';
  193. }
  194. return Math.floor(
  195. {
  196. milliseconds,
  197. seconds,
  198. minutes,
  199. hours,
  200. days,
  201. months,
  202. years,
  203. }[scale]
  204. );
  205. },
  206. today() {
  207. const vals = this.get_date_values(new Date()).slice(0, 3);
  208. return new Date(...vals);
  209. },
  210. now() {
  211. return new Date();
  212. },
  213. add(date, qty, scale) {
  214. qty = parseInt(qty, 10);
  215. const vals = [
  216. date.getFullYear() + (scale === YEAR ? qty : 0),
  217. date.getMonth() + (scale === MONTH ? qty : 0),
  218. date.getDate() + (scale === DAY ? qty : 0),
  219. date.getHours() + (scale === HOUR ? qty : 0),
  220. date.getMinutes() + (scale === MINUTE ? qty : 0),
  221. date.getSeconds() + (scale === SECOND ? qty : 0),
  222. date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),
  223. ];
  224. return new Date(...vals);
  225. },
  226. start_of(date, scale) {
  227. const scores = {
  228. [YEAR]: 6,
  229. [MONTH]: 5,
  230. [DAY]: 4,
  231. [HOUR]: 3,
  232. [MINUTE]: 2,
  233. [SECOND]: 1,
  234. [MILLISECOND]: 0,
  235. };
  236. function should_reset(_scale) {
  237. const max_score = scores[scale];
  238. return scores[_scale] <= max_score;
  239. }
  240. const vals = [
  241. date.getFullYear(),
  242. should_reset(YEAR) ? 0 : date.getMonth(),
  243. should_reset(MONTH) ? 1 : date.getDate(),
  244. should_reset(DAY) ? 0 : date.getHours(),
  245. should_reset(HOUR) ? 0 : date.getMinutes(),
  246. should_reset(MINUTE) ? 0 : date.getSeconds(),
  247. should_reset(SECOND) ? 0 : date.getMilliseconds(),
  248. ];
  249. return new Date(...vals);
  250. },
  251. clone(date) {
  252. return new Date(...this.get_date_values(date));
  253. },
  254. get_date_values(date) {
  255. return [
  256. date.getFullYear(),
  257. date.getMonth(),
  258. date.getDate(),
  259. date.getHours(),
  260. date.getMinutes(),
  261. date.getSeconds(),
  262. date.getMilliseconds(),
  263. ];
  264. },
  265. get_days_in_month(date) {
  266. const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  267. const month = date.getMonth();
  268. if (month !== 1) {
  269. return no_of_days[month];
  270. }
  271. // Feb
  272. const year = date.getFullYear();
  273. if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
  274. return 29;
  275. }
  276. return 28;
  277. },
  278. };
  279. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
  280. function padStart(str, targetLength, padString) {
  281. str = str + '';
  282. targetLength = targetLength >> 0;
  283. padString = String(typeof padString !== 'undefined' ? padString : ' ');
  284. if (str.length > targetLength) {
  285. return String(str);
  286. } else {
  287. targetLength = targetLength - str.length;
  288. if (targetLength > padString.length) {
  289. padString += padString.repeat(targetLength / padString.length);
  290. }
  291. return padString.slice(0, targetLength) + String(str);
  292. }
  293. }
  294. function $(expr, con) {
  295. return typeof expr === 'string'
  296. ? (con || document).querySelector(expr)
  297. : expr || null;
  298. }
  299. function createSVG(tag, attrs) {
  300. const elem = document.createElementNS('http://www.w3.org/2000/svg', tag);
  301. for (let attr in attrs) {
  302. if (attr === 'append_to') {
  303. const parent = attrs.append_to;
  304. parent.appendChild(elem);
  305. } else if (attr === 'innerHTML') {
  306. elem.innerHTML = attrs.innerHTML;
  307. } else {
  308. elem.setAttribute(attr, attrs[attr]);
  309. }
  310. }
  311. return elem;
  312. }
  313. function animateSVG(svgElement, attr, from, to) {
  314. const animatedSvgElement = getAnimationElement(svgElement, attr, from, to);
  315. if (animatedSvgElement === svgElement) {
  316. // triggered 2nd time programmatically
  317. // trigger artificial click event
  318. const event = document.createEvent('HTMLEvents');
  319. event.initEvent('click', true, true);
  320. event.eventName = 'click';
  321. animatedSvgElement.dispatchEvent(event);
  322. }
  323. }
  324. function getAnimationElement(
  325. svgElement,
  326. attr,
  327. from,
  328. to,
  329. dur = '0.4s',
  330. begin = '0.1s'
  331. ) {
  332. const animEl = svgElement.querySelector('animate');
  333. if (animEl) {
  334. $.attr(animEl, {
  335. attributeName: attr,
  336. from,
  337. to,
  338. dur,
  339. begin: 'click + ' + begin, // artificial click
  340. });
  341. return svgElement;
  342. }
  343. const animateElement = createSVG('animate', {
  344. attributeName: attr,
  345. from,
  346. to,
  347. dur,
  348. begin,
  349. calcMode: 'spline',
  350. values: from + ';' + to,
  351. keyTimes: '0; 1',
  352. keySplines: cubic_bezier('ease-out'),
  353. });
  354. svgElement.appendChild(animateElement);
  355. return svgElement;
  356. }
  357. function cubic_bezier(name) {
  358. return {
  359. ease: '.25 .1 .25 1',
  360. linear: '0 0 1 1',
  361. 'ease-in': '.42 0 1 1',
  362. 'ease-out': '0 0 .58 1',
  363. 'ease-in-out': '.42 0 .58 1',
  364. }[name];
  365. }
  366. $.on = (element, event, selector, callback) => {
  367. if (!callback) {
  368. callback = selector;
  369. $.bind(element, event, callback);
  370. } else {
  371. $.delegate(element, event, selector, callback);
  372. }
  373. };
  374. $.off = (element, event, handler) => {
  375. element.removeEventListener(event, handler);
  376. };
  377. $.bind = (element, event, callback) => {
  378. event.split(/\s+/).forEach(function (event) {
  379. element.addEventListener(event, callback);
  380. });
  381. };
  382. $.delegate = (element, event, selector, callback) => {
  383. element.addEventListener(event, function (e) {
  384. const delegatedTarget = e.target.closest(selector);
  385. if (delegatedTarget) {
  386. e.delegatedTarget = delegatedTarget;
  387. callback.call(this, e, delegatedTarget);
  388. }
  389. });
  390. };
  391. $.closest = (selector, element) => {
  392. if (!element) return null;
  393. if (element.matches(selector)) {
  394. return element;
  395. }
  396. return $.closest(selector, element.parentNode);
  397. };
  398. $.attr = (element, attr, value) => {
  399. if (!value && typeof attr === 'string') {
  400. return element.getAttribute(attr);
  401. }
  402. if (typeof attr === 'object') {
  403. for (let key in attr) {
  404. $.attr(element, key, attr[key]);
  405. }
  406. return;
  407. }
  408. element.setAttribute(attr, value);
  409. };
  410. class Bar {
  411. constructor(gantt, task) {
  412. this.set_defaults(gantt, task);
  413. this.prepare();
  414. this.draw();
  415. this.bind();
  416. }
  417. set_defaults(gantt, task) {
  418. this.action_completed = false;
  419. this.gantt = gantt;
  420. this.task = task;
  421. }
  422. prepare() {
  423. this.prepare_values();
  424. this.prepare_helpers();
  425. }
  426. prepare_values() {
  427. this.invalid = this.task.invalid;
  428. this.height = this.gantt.options.bar_height;
  429. this.x = this.compute_x();
  430. this.y = this.compute_y();
  431. this.corner_radius = this.gantt.options.bar_corner_radius;
  432. this.duration =
  433. date_utils.diff(this.task._end, this.task._start, 'hour') /
  434. this.gantt.options.step;
  435. this.width = this.gantt.options.column_width * this.duration;
  436. this.progress_width =
  437. this.gantt.options.column_width *
  438. this.duration *
  439. (this.task.progress / 100) || 0;
  440. this.group = createSVG('g', {
  441. class: 'bar-wrapper ' + (this.task.custom_class || ''),
  442. 'data-id': this.task.id,
  443. });
  444. this.bar_group = createSVG('g', {
  445. class: 'bar-group',
  446. append_to: this.group,
  447. });
  448. this.handle_group = createSVG('g', {
  449. class: 'handle-group',
  450. append_to: this.group,
  451. });
  452. }
  453. prepare_helpers() {
  454. SVGElement.prototype.getX = function () {
  455. return +this.getAttribute('x');
  456. };
  457. SVGElement.prototype.getY = function () {
  458. return +this.getAttribute('y');
  459. };
  460. SVGElement.prototype.getWidth = function () {
  461. return +this.getAttribute('width');
  462. };
  463. SVGElement.prototype.getHeight = function () {
  464. return +this.getAttribute('height');
  465. };
  466. SVGElement.prototype.getEndX = function () {
  467. return this.getX() + this.getWidth();
  468. };
  469. }
  470. draw() {
  471. this.draw_bar();
  472. this.draw_progress_bar();
  473. this.draw_label();
  474. this.draw_resize_handles();
  475. }
  476. draw_bar() {
  477. this.$bar = createSVG('rect', {
  478. x: this.x,
  479. y: this.y,
  480. width: this.width,
  481. height: this.height,
  482. rx: this.corner_radius,
  483. ry: this.corner_radius,
  484. class: 'bar',
  485. append_to: this.bar_group,
  486. });
  487. animateSVG(this.$bar, 'width', 0, this.width);
  488. if (this.invalid) {
  489. this.$bar.classList.add('bar-invalid');
  490. }
  491. }
  492. draw_progress_bar() {
  493. if (this.invalid) return;
  494. this.$bar_progress = createSVG('rect', {
  495. x: this.x,
  496. y: this.y,
  497. width: this.progress_width,
  498. height: this.height,
  499. rx: this.corner_radius,
  500. ry: this.corner_radius,
  501. class: 'bar-progress',
  502. append_to: this.bar_group,
  503. });
  504. animateSVG(this.$bar_progress, 'width', 0, this.progress_width);
  505. }
  506. draw_label() {
  507. createSVG('text', {
  508. x: this.x + this.width / 2,
  509. y: this.y + this.height / 2,
  510. innerHTML: this.task.name,
  511. class: 'bar-label',
  512. append_to: this.bar_group,
  513. });
  514. // labels get BBox in the next tick
  515. requestAnimationFrame(() => this.update_label_position());
  516. }
  517. draw_resize_handles() {
  518. if (this.invalid) return;
  519. const bar = this.$bar;
  520. const handle_width = 8;
  521. createSVG('rect', {
  522. x: bar.getX() + bar.getWidth() - 9,
  523. y: bar.getY() + 1,
  524. width: handle_width,
  525. height: this.height - 2,
  526. rx: this.corner_radius,
  527. ry: this.corner_radius,
  528. class: 'handle right',
  529. append_to: this.handle_group,
  530. });
  531. createSVG('rect', {
  532. x: bar.getX() + 1,
  533. y: bar.getY() + 1,
  534. width: handle_width,
  535. height: this.height - 2,
  536. rx: this.corner_radius,
  537. ry: this.corner_radius,
  538. class: 'handle left',
  539. append_to: this.handle_group,
  540. });
  541. if (this.task.progress && this.task.progress < 100) {
  542. this.$handle_progress = createSVG('polygon', {
  543. points: this.get_progress_polygon_points().join(','),
  544. class: 'handle progress',
  545. append_to: this.handle_group,
  546. });
  547. }
  548. }
  549. get_progress_polygon_points() {
  550. const bar_progress = this.$bar_progress;
  551. return [
  552. bar_progress.getEndX() - 5,
  553. bar_progress.getY() + bar_progress.getHeight(),
  554. bar_progress.getEndX() + 5,
  555. bar_progress.getY() + bar_progress.getHeight(),
  556. bar_progress.getEndX(),
  557. bar_progress.getY() + bar_progress.getHeight() - 8.66,
  558. ];
  559. }
  560. bind() {
  561. if (this.invalid) return;
  562. this.setup_click_event();
  563. }
  564. setup_click_event() {
  565. $.on(this.group, 'focus ' + this.gantt.options.popup_trigger, (e) => {
  566. if (this.action_completed) {
  567. // just finished a move action, wait for a few seconds
  568. return;
  569. }
  570. this.show_popup();
  571. this.gantt.unselect_all();
  572. this.group.classList.add('active');
  573. });
  574. $.on(this.group, 'dblclick', (e) => {
  575. if (this.action_completed) {
  576. // just finished a move action, wait for a few seconds
  577. return;
  578. }
  579. this.gantt.trigger_event('click', [this.task]);
  580. });
  581. }
  582. show_popup() {
  583. if (this.gantt.bar_being_dragged) return;
  584. const start_date = date_utils.format(
  585. this.task._start,
  586. 'MMM D',
  587. this.gantt.options.language
  588. );
  589. const end_date = date_utils.format(
  590. date_utils.add(this.task._end, -1, 'second'),
  591. 'MMM D',
  592. this.gantt.options.language
  593. );
  594. const subtitle = start_date + ' - ' + end_date;
  595. this.gantt.show_popup({
  596. target_element: this.$bar,
  597. title: this.task.name,
  598. subtitle: subtitle,
  599. task: this.task,
  600. });
  601. }
  602. update_bar_position({ x = null, width = null }) {
  603. const bar = this.$bar;
  604. if (x) {
  605. // get all x values of parent task
  606. const xs = this.task.dependencies.map((dep) => {
  607. return this.gantt.get_bar(dep).$bar.getX();
  608. });
  609. // child task must not go before parent
  610. const valid_x = xs.reduce((prev, curr) => {
  611. return x >= curr;
  612. }, x);
  613. if (!valid_x) {
  614. width = null;
  615. return;
  616. }
  617. this.update_attr(bar, 'x', x);
  618. }
  619. if (width && width >= this.gantt.options.column_width) {
  620. this.update_attr(bar, 'width', width);
  621. }
  622. this.update_label_position();
  623. this.update_handle_position();
  624. this.update_progressbar_position();
  625. this.update_arrow_position();
  626. }
  627. date_changed() {
  628. let changed = false;
  629. const { new_start_date, new_end_date } = this.compute_start_end_date();
  630. if (Number(this.task._start) !== Number(new_start_date)) {
  631. changed = true;
  632. this.task._start = new_start_date;
  633. }
  634. if (Number(this.task._end) !== Number(new_end_date)) {
  635. changed = true;
  636. this.task._end = new_end_date;
  637. }
  638. if (!changed) return;
  639. this.gantt.trigger_event('date_change', [
  640. this.task,
  641. new_start_date,
  642. date_utils.add(new_end_date, -1, 'second'),
  643. ]);
  644. }
  645. progress_changed() {
  646. const new_progress = this.compute_progress();
  647. this.task.progress = new_progress;
  648. this.gantt.trigger_event('progress_change', [this.task, new_progress]);
  649. }
  650. set_action_completed() {
  651. this.action_completed = true;
  652. setTimeout(() => (this.action_completed = false), 1000);
  653. }
  654. compute_start_end_date() {
  655. const bar = this.$bar;
  656. const x_in_units = bar.getX() / this.gantt.options.column_width;
  657. const new_start_date = date_utils.add(
  658. this.gantt.gantt_start,
  659. x_in_units * this.gantt.options.step,
  660. 'hour'
  661. );
  662. const width_in_units = bar.getWidth() / this.gantt.options.column_width;
  663. const new_end_date = date_utils.add(
  664. new_start_date,
  665. width_in_units * this.gantt.options.step,
  666. 'hour'
  667. );
  668. return { new_start_date, new_end_date };
  669. }
  670. compute_progress() {
  671. const progress =
  672. (this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100;
  673. return parseInt(progress, 10);
  674. }
  675. compute_x() {
  676. const { step, column_width } = this.gantt.options;
  677. const task_start = this.task._start;
  678. const gantt_start = this.gantt.gantt_start;
  679. const diff = date_utils.diff(task_start, gantt_start, 'hour');
  680. let x = (diff / step) * column_width;
  681. if (this.gantt.view_is('Month')) {
  682. const diff = date_utils.diff(task_start, gantt_start, 'day');
  683. x = (diff * column_width) / 30;
  684. }
  685. return x;
  686. }
  687. compute_y() {
  688. return (
  689. this.gantt.options.header_height +
  690. this.gantt.options.padding +
  691. this.task._index * (this.height + this.gantt.options.padding)
  692. );
  693. }
  694. get_snap_position(dx) {
  695. let odx = dx,
  696. rem,
  697. position;
  698. if (this.gantt.view_is('Week')) {
  699. rem = dx % (this.gantt.options.column_width / 7);
  700. position =
  701. odx -
  702. rem +
  703. (rem < this.gantt.options.column_width / 14
  704. ? 0
  705. : this.gantt.options.column_width / 7);
  706. } else if (this.gantt.view_is('Month')) {
  707. rem = dx % (this.gantt.options.column_width / 30);
  708. position =
  709. odx -
  710. rem +
  711. (rem < this.gantt.options.column_width / 60
  712. ? 0
  713. : this.gantt.options.column_width / 30);
  714. } else {
  715. rem = dx % this.gantt.options.column_width;
  716. position =
  717. odx -
  718. rem +
  719. (rem < this.gantt.options.column_width / 2
  720. ? 0
  721. : this.gantt.options.column_width);
  722. }
  723. return position;
  724. }
  725. update_attr(element, attr, value) {
  726. value = +value;
  727. if (!isNaN(value)) {
  728. element.setAttribute(attr, value);
  729. }
  730. return element;
  731. }
  732. update_progressbar_position() {
  733. this.$bar_progress.setAttribute('x', this.$bar.getX());
  734. this.$bar_progress.setAttribute(
  735. 'width',
  736. this.$bar.getWidth() * (this.task.progress / 100)
  737. );
  738. }
  739. update_label_position() {
  740. const bar = this.$bar,
  741. label = this.group.querySelector('.bar-label');
  742. if (label.getBBox().width > bar.getWidth()) {
  743. label.classList.add('big');
  744. label.setAttribute('x', bar.getX() + bar.getWidth() + 5);
  745. } else {
  746. label.classList.remove('big');
  747. label.setAttribute('x', bar.getX() + bar.getWidth() / 2);
  748. }
  749. }
  750. update_handle_position() {
  751. const bar = this.$bar;
  752. this.handle_group
  753. .querySelector('.handle.left')
  754. .setAttribute('x', bar.getX() + 1);
  755. this.handle_group
  756. .querySelector('.handle.right')
  757. .setAttribute('x', bar.getEndX() - 9);
  758. const handle = this.group.querySelector('.handle.progress');
  759. handle &&
  760. handle.setAttribute('points', this.get_progress_polygon_points());
  761. }
  762. update_arrow_position() {
  763. this.arrows = this.arrows || [];
  764. for (let arrow of this.arrows) {
  765. arrow.update();
  766. }
  767. }
  768. }
  769. class Arrow {
  770. constructor(gantt, from_task, to_task) {
  771. this.gantt = gantt;
  772. this.from_task = from_task;
  773. this.to_task = to_task;
  774. this.calculate_path();
  775. this.draw();
  776. }
  777. calculate_path() {
  778. let start_x =
  779. this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2;
  780. const condition = () =>
  781. this.to_task.$bar.getX() < start_x + this.gantt.options.padding &&
  782. start_x > this.from_task.$bar.getX() + this.gantt.options.padding;
  783. while (condition()) {
  784. start_x -= 10;
  785. }
  786. const start_y =
  787. this.gantt.options.header_height +
  788. this.gantt.options.bar_height +
  789. (this.gantt.options.padding + this.gantt.options.bar_height) *
  790. this.from_task.task._index +
  791. this.gantt.options.padding;
  792. const end_x = this.to_task.$bar.getX() - this.gantt.options.padding / 2;
  793. const end_y =
  794. this.gantt.options.header_height +
  795. this.gantt.options.bar_height / 2 +
  796. (this.gantt.options.padding + this.gantt.options.bar_height) *
  797. this.to_task.task._index +
  798. this.gantt.options.padding;
  799. const from_is_below_to =
  800. this.from_task.task._index > this.to_task.task._index;
  801. const curve = this.gantt.options.arrow_curve;
  802. const clockwise = from_is_below_to ? 1 : 0;
  803. const curve_y = from_is_below_to ? -curve : curve;
  804. const offset = from_is_below_to
  805. ? end_y + this.gantt.options.arrow_curve
  806. : end_y - this.gantt.options.arrow_curve;
  807. this.path = `
  808. M ${start_x} ${start_y}
  809. V ${offset}
  810. a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
  811. L ${end_x} ${end_y}
  812. m -5 -5
  813. l 5 5
  814. l -5 5`;
  815. if (
  816. this.to_task.$bar.getX() <
  817. this.from_task.$bar.getX() + this.gantt.options.padding
  818. ) {
  819. const down_1 = this.gantt.options.padding / 2 - curve;
  820. const down_2 =
  821. this.to_task.$bar.getY() +
  822. this.to_task.$bar.getHeight() / 2 -
  823. curve_y;
  824. const left = this.to_task.$bar.getX() - this.gantt.options.padding;
  825. this.path = `
  826. M ${start_x} ${start_y}
  827. v ${down_1}
  828. a ${curve} ${curve} 0 0 1 -${curve} ${curve}
  829. H ${left}
  830. a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}
  831. V ${down_2}
  832. a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
  833. L ${end_x} ${end_y}
  834. m -5 -5
  835. l 5 5
  836. l -5 5`;
  837. }
  838. }
  839. draw() {
  840. this.element = createSVG('path', {
  841. d: this.path,
  842. 'data-from': this.from_task.task.id,
  843. 'data-to': this.to_task.task.id,
  844. });
  845. }
  846. update() {
  847. this.calculate_path();
  848. this.element.setAttribute('d', this.path);
  849. }
  850. }
  851. class Popup {
  852. constructor(parent, custom_html) {
  853. this.parent = parent;
  854. this.custom_html = custom_html;
  855. this.make();
  856. }
  857. make() {
  858. this.parent.innerHTML = `
  859. <div class="title"></div>
  860. <div class="subtitle"></div>
  861. <div class="pointer"></div>
  862. `;
  863. this.hide();
  864. this.title = this.parent.querySelector('.title');
  865. this.subtitle = this.parent.querySelector('.subtitle');
  866. this.pointer = this.parent.querySelector('.pointer');
  867. }
  868. show(options) {
  869. if (!options.target_element) {
  870. throw new Error('target_element is required to show popup');
  871. }
  872. if (!options.position) {
  873. options.position = 'left';
  874. }
  875. const target_element = options.target_element;
  876. if (this.custom_html) {
  877. let html = this.custom_html(options.task);
  878. html += '<div class="pointer"></div>';
  879. this.parent.innerHTML = html;
  880. this.pointer = this.parent.querySelector('.pointer');
  881. } else {
  882. // set data
  883. this.title.innerHTML = options.title;
  884. this.subtitle.innerHTML = options.subtitle;
  885. this.parent.style.width = this.parent.clientWidth + 'px';
  886. }
  887. // set position
  888. let position_meta;
  889. if (target_element instanceof HTMLElement) {
  890. position_meta = target_element.getBoundingClientRect();
  891. } else if (target_element instanceof SVGElement) {
  892. position_meta = options.target_element.getBBox();
  893. }
  894. if (options.position === 'left') {
  895. this.parent.style.left =
  896. position_meta.x + (position_meta.width + 10) + 'px';
  897. this.parent.style.top = position_meta.y + 'px';
  898. this.pointer.style.transform = 'rotateZ(90deg)';
  899. this.pointer.style.left = '-7px';
  900. this.pointer.style.top = '2px';
  901. }
  902. // show
  903. this.parent.style.opacity = 1;
  904. }
  905. hide() {
  906. this.parent.style.opacity = 0;
  907. this.parent.style.left = 0;
  908. }
  909. }
  910. const VIEW_MODE = {
  911. QUARTER_DAY: 'Quarter Day',
  912. HALF_DAY: 'Half Day',
  913. DAY: 'Day',
  914. WEEK: 'Week',
  915. MONTH: 'Month',
  916. YEAR: 'Year',
  917. };
  918. class Gantt {
  919. constructor(wrapper, tasks, options) {
  920. this.setup_wrapper(wrapper);
  921. this.setup_options(options);
  922. this.setup_tasks(tasks);
  923. // initialize with default view mode
  924. this.change_view_mode();
  925. this.bind_events();
  926. }
  927. setup_wrapper(element) {
  928. let svg_element, wrapper_element;
  929. // CSS Selector is passed
  930. if (typeof element === 'string') {
  931. element = document.querySelector(element);
  932. }
  933. // get the SVGElement
  934. if (element instanceof HTMLElement) {
  935. wrapper_element = element;
  936. svg_element = element.querySelector('svg');
  937. } else if (element instanceof SVGElement) {
  938. svg_element = element;
  939. } else {
  940. throw new TypeError(
  941. 'XhiveFramework Gantt only supports usage of a string CSS selector,' +
  942. " HTML DOM element or SVG DOM element for the 'element' parameter"
  943. );
  944. }
  945. // svg element
  946. if (!svg_element) {
  947. // create it
  948. this.$svg = createSVG('svg', {
  949. append_to: wrapper_element,
  950. class: 'gantt',
  951. });
  952. } else {
  953. this.$svg = svg_element;
  954. this.$svg.classList.add('gantt');
  955. }
  956. // wrapper element
  957. this.$container = document.createElement('div');
  958. this.$container.classList.add('gantt-container');
  959. const parent_element = this.$svg.parentElement;
  960. parent_element.appendChild(this.$container);
  961. this.$container.appendChild(this.$svg);
  962. // popup wrapper
  963. this.popup_wrapper = document.createElement('div');
  964. this.popup_wrapper.classList.add('popup-wrapper');
  965. this.$container.appendChild(this.popup_wrapper);
  966. }
  967. setup_options(options) {
  968. const default_options = {
  969. header_height: 50,
  970. column_width: 30,
  971. step: 24,
  972. view_modes: [...Object.values(VIEW_MODE)],
  973. bar_height: 20,
  974. bar_corner_radius: 3,
  975. arrow_curve: 5,
  976. padding: 18,
  977. view_mode: 'Day',
  978. date_format: 'YYYY-MM-DD',
  979. popup_trigger: 'click',
  980. custom_popup_html: null,
  981. language: 'en',
  982. };
  983. this.options = Object.assign({}, default_options, options);
  984. }
  985. setup_tasks(tasks) {
  986. // prepare tasks
  987. this.tasks = tasks.map((task, i) => {
  988. // convert to Date objects
  989. task._start = date_utils.parse(task.start);
  990. task._end = date_utils.parse(task.end);
  991. // make task invalid if duration too large
  992. if (date_utils.diff(task._end, task._start, 'year') > 10) {
  993. task.end = null;
  994. }
  995. // cache index
  996. task._index = i;
  997. // invalid dates
  998. if (!task.start && !task.end) {
  999. const today = date_utils.today();
  1000. task._start = today;
  1001. task._end = date_utils.add(today, 2, 'day');
  1002. }
  1003. if (!task.start && task.end) {
  1004. task._start = date_utils.add(task._end, -2, 'day');
  1005. }
  1006. if (task.start && !task.end) {
  1007. task._end = date_utils.add(task._start, 2, 'day');
  1008. }
  1009. // if hours is not set, assume the last day is full day
  1010. // e.g: 2018-09-09 becomes 2018-09-09 23:59:59
  1011. const task_end_values = date_utils.get_date_values(task._end);
  1012. if (task_end_values.slice(3).every((d) => d === 0)) {
  1013. task._end = date_utils.add(task._end, 24, 'hour');
  1014. }
  1015. // invalid flag
  1016. if (!task.start || !task.end) {
  1017. task.invalid = true;
  1018. }
  1019. // dependencies
  1020. if (typeof task.dependencies === 'string' || !task.dependencies) {
  1021. let deps = [];
  1022. if (task.dependencies) {
  1023. deps = task.dependencies
  1024. .split(',')
  1025. .map((d) => d.trim())
  1026. .filter((d) => d);
  1027. }
  1028. task.dependencies = deps;
  1029. }
  1030. // uids
  1031. if (!task.id) {
  1032. task.id = generate_id(task);
  1033. }
  1034. return task;
  1035. });
  1036. this.setup_dependencies();
  1037. }
  1038. setup_dependencies() {
  1039. this.dependency_map = {};
  1040. for (let t of this.tasks) {
  1041. for (let d of t.dependencies) {
  1042. this.dependency_map[d] = this.dependency_map[d] || [];
  1043. this.dependency_map[d].push(t.id);
  1044. }
  1045. }
  1046. }
  1047. refresh(tasks) {
  1048. this.setup_tasks(tasks);
  1049. this.change_view_mode();
  1050. }
  1051. change_view_mode(mode = this.options.view_mode) {
  1052. this.update_view_scale(mode);
  1053. this.setup_dates();
  1054. this.render();
  1055. // fire viewmode_change event
  1056. this.trigger_event('view_change', [mode]);
  1057. }
  1058. update_view_scale(view_mode) {
  1059. this.options.view_mode = view_mode;
  1060. if (view_mode === VIEW_MODE.DAY) {
  1061. this.options.step = 24;
  1062. this.options.column_width = 38;
  1063. } else if (view_mode === VIEW_MODE.HALF_DAY) {
  1064. this.options.step = 24 / 2;
  1065. this.options.column_width = 38;
  1066. } else if (view_mode === VIEW_MODE.QUARTER_DAY) {
  1067. this.options.step = 24 / 4;
  1068. this.options.column_width = 38;
  1069. } else if (view_mode === VIEW_MODE.WEEK) {
  1070. this.options.step = 24 * 7;
  1071. this.options.column_width = 140;
  1072. } else if (view_mode === VIEW_MODE.MONTH) {
  1073. this.options.step = 24 * 30;
  1074. this.options.column_width = 120;
  1075. } else if (view_mode === VIEW_MODE.YEAR) {
  1076. this.options.step = 24 * 365;
  1077. this.options.column_width = 120;
  1078. }
  1079. }
  1080. setup_dates() {
  1081. this.setup_gantt_dates();
  1082. this.setup_date_values();
  1083. }
  1084. setup_gantt_dates() {
  1085. this.gantt_start = this.gantt_end = null;
  1086. for (let task of this.tasks) {
  1087. // set global start and end date
  1088. if (!this.gantt_start || task._start < this.gantt_start) {
  1089. this.gantt_start = task._start;
  1090. }
  1091. if (!this.gantt_end || task._end > this.gantt_end) {
  1092. this.gantt_end = task._end;
  1093. }
  1094. }
  1095. this.gantt_start = date_utils.start_of(this.gantt_start, 'day');
  1096. this.gantt_end = date_utils.start_of(this.gantt_end, 'day');
  1097. // add date padding on both sides
  1098. if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY])) {
  1099. this.gantt_start = date_utils.add(this.gantt_start, -7, 'day');
  1100. this.gantt_end = date_utils.add(this.gantt_end, 7, 'day');
  1101. } else if (this.view_is(VIEW_MODE.MONTH)) {
  1102. this.gantt_start = date_utils.start_of(this.gantt_start, 'year');
  1103. this.gantt_end = date_utils.add(this.gantt_end, 1, 'year');
  1104. } else if (this.view_is(VIEW_MODE.YEAR)) {
  1105. this.gantt_start = date_utils.add(this.gantt_start, -2, 'year');
  1106. this.gantt_end = date_utils.add(this.gantt_end, 2, 'year');
  1107. } else {
  1108. this.gantt_start = date_utils.add(this.gantt_start, -1, 'month');
  1109. this.gantt_end = date_utils.add(this.gantt_end, 1, 'month');
  1110. }
  1111. }
  1112. setup_date_values() {
  1113. this.dates = [];
  1114. let cur_date = null;
  1115. while (cur_date === null || cur_date < this.gantt_end) {
  1116. if (!cur_date) {
  1117. cur_date = date_utils.clone(this.gantt_start);
  1118. } else {
  1119. if (this.view_is(VIEW_MODE.YEAR)) {
  1120. cur_date = date_utils.add(cur_date, 1, 'year');
  1121. } else if (this.view_is(VIEW_MODE.MONTH)) {
  1122. cur_date = date_utils.add(cur_date, 1, 'month');
  1123. } else {
  1124. cur_date = date_utils.add(
  1125. cur_date,
  1126. this.options.step,
  1127. 'hour'
  1128. );
  1129. }
  1130. }
  1131. this.dates.push(cur_date);
  1132. }
  1133. }
  1134. bind_events() {
  1135. this.bind_grid_click();
  1136. this.bind_bar_events();
  1137. }
  1138. render() {
  1139. this.clear();
  1140. this.setup_layers();
  1141. this.make_grid();
  1142. this.make_dates();
  1143. this.make_bars();
  1144. this.make_arrows();
  1145. this.map_arrows_on_bars();
  1146. this.set_width();
  1147. this.set_scroll_position();
  1148. }
  1149. setup_layers() {
  1150. this.layers = {};
  1151. const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];
  1152. // make group layers
  1153. for (let layer of layers) {
  1154. this.layers[layer] = createSVG('g', {
  1155. class: layer,
  1156. append_to: this.$svg,
  1157. });
  1158. }
  1159. }
  1160. make_grid() {
  1161. this.make_grid_background();
  1162. this.make_grid_rows();
  1163. this.make_grid_header();
  1164. this.make_grid_ticks();
  1165. this.make_grid_highlights();
  1166. }
  1167. make_grid_background() {
  1168. const grid_width = this.dates.length * this.options.column_width;
  1169. const grid_height =
  1170. this.options.header_height +
  1171. this.options.padding +
  1172. (this.options.bar_height + this.options.padding) *
  1173. this.tasks.length;
  1174. createSVG('rect', {
  1175. x: 0,
  1176. y: 0,
  1177. width: grid_width,
  1178. height: grid_height,
  1179. class: 'grid-background',
  1180. append_to: this.layers.grid,
  1181. });
  1182. $.attr(this.$svg, {
  1183. height: grid_height + this.options.padding + 100,
  1184. width: '100%',
  1185. });
  1186. }
  1187. make_grid_rows() {
  1188. const rows_layer = createSVG('g', { append_to: this.layers.grid });
  1189. const lines_layer = createSVG('g', { append_to: this.layers.grid });
  1190. const row_width = this.dates.length * this.options.column_width;
  1191. const row_height = this.options.bar_height + this.options.padding;
  1192. let row_y = this.options.header_height + this.options.padding / 2;
  1193. for (let task of this.tasks) {
  1194. createSVG('rect', {
  1195. x: 0,
  1196. y: row_y,
  1197. width: row_width,
  1198. height: row_height,
  1199. class: 'grid-row',
  1200. append_to: rows_layer,
  1201. });
  1202. createSVG('line', {
  1203. x1: 0,
  1204. y1: row_y + row_height,
  1205. x2: row_width,
  1206. y2: row_y + row_height,
  1207. class: 'row-line',
  1208. append_to: lines_layer,
  1209. });
  1210. row_y += this.options.bar_height + this.options.padding;
  1211. }
  1212. }
  1213. make_grid_header() {
  1214. const header_width = this.dates.length * this.options.column_width;
  1215. const header_height = this.options.header_height + 10;
  1216. createSVG('rect', {
  1217. x: 0,
  1218. y: 0,
  1219. width: header_width,
  1220. height: header_height,
  1221. class: 'grid-header',
  1222. append_to: this.layers.grid,
  1223. });
  1224. }
  1225. make_grid_ticks() {
  1226. let tick_x = 0;
  1227. let tick_y = this.options.header_height + this.options.padding / 2;
  1228. let tick_height =
  1229. (this.options.bar_height + this.options.padding) *
  1230. this.tasks.length;
  1231. for (let date of this.dates) {
  1232. let tick_class = 'tick';
  1233. // thick tick for monday
  1234. if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {
  1235. tick_class += ' thick';
  1236. }
  1237. // thick tick for first week
  1238. if (
  1239. this.view_is(VIEW_MODE.WEEK) &&
  1240. date.getDate() >= 1 &&
  1241. date.getDate() < 8
  1242. ) {
  1243. tick_class += ' thick';
  1244. }
  1245. // thick ticks for quarters
  1246. if (
  1247. this.view_is(VIEW_MODE.MONTH) &&
  1248. (date.getMonth() + 1) % 3 === 0
  1249. ) {
  1250. tick_class += ' thick';
  1251. }
  1252. createSVG('path', {
  1253. d: `M ${tick_x} ${tick_y} v ${tick_height}`,
  1254. class: tick_class,
  1255. append_to: this.layers.grid,
  1256. });
  1257. if (this.view_is(VIEW_MODE.MONTH)) {
  1258. tick_x +=
  1259. (date_utils.get_days_in_month(date) *
  1260. this.options.column_width) /
  1261. 30;
  1262. } else {
  1263. tick_x += this.options.column_width;
  1264. }
  1265. }
  1266. }
  1267. make_grid_highlights() {
  1268. // highlight today's date
  1269. if (this.view_is(VIEW_MODE.DAY)) {
  1270. const x =
  1271. (date_utils.diff(date_utils.today(), this.gantt_start, 'hour') /
  1272. this.options.step) *
  1273. this.options.column_width;
  1274. const y = 0;
  1275. const width = this.options.column_width;
  1276. const height =
  1277. (this.options.bar_height + this.options.padding) *
  1278. this.tasks.length +
  1279. this.options.header_height +
  1280. this.options.padding / 2;
  1281. createSVG('rect', {
  1282. x,
  1283. y,
  1284. width,
  1285. height,
  1286. class: 'today-highlight',
  1287. append_to: this.layers.grid,
  1288. });
  1289. }
  1290. }
  1291. make_dates() {
  1292. for (let date of this.get_dates_to_draw()) {
  1293. createSVG('text', {
  1294. x: date.lower_x,
  1295. y: date.lower_y,
  1296. innerHTML: date.lower_text,
  1297. class: 'lower-text',
  1298. append_to: this.layers.date,
  1299. });
  1300. if (date.upper_text) {
  1301. const $upper_text = createSVG('text', {
  1302. x: date.upper_x,
  1303. y: date.upper_y,
  1304. innerHTML: date.upper_text,
  1305. class: 'upper-text',
  1306. append_to: this.layers.date,
  1307. });
  1308. // remove out-of-bound dates
  1309. if (
  1310. $upper_text.getBBox().x2 > this.layers.grid.getBBox().width
  1311. ) {
  1312. $upper_text.remove();
  1313. }
  1314. }
  1315. }
  1316. }
  1317. get_dates_to_draw() {
  1318. let last_date = null;
  1319. const dates = this.dates.map((date, i) => {
  1320. const d = this.get_date_info(date, last_date, i);
  1321. last_date = date;
  1322. return d;
  1323. });
  1324. return dates;
  1325. }
  1326. get_date_info(date, last_date, i) {
  1327. if (!last_date) {
  1328. last_date = date_utils.add(date, 1, 'year');
  1329. }
  1330. const date_text = {
  1331. 'Quarter Day_lower': date_utils.format(
  1332. date,
  1333. 'HH',
  1334. this.options.language
  1335. ),
  1336. 'Half Day_lower': date_utils.format(
  1337. date,
  1338. 'HH',
  1339. this.options.language
  1340. ),
  1341. Day_lower:
  1342. date.getDate() !== last_date.getDate()
  1343. ? date_utils.format(date, 'D', this.options.language)
  1344. : '',
  1345. Week_lower:
  1346. date.getMonth() !== last_date.getMonth()
  1347. ? date_utils.format(date, 'D MMM', this.options.language)
  1348. : date_utils.format(date, 'D', this.options.language),
  1349. Month_lower: date_utils.format(date, 'MMMM', this.options.language),
  1350. Year_lower: date_utils.format(date, 'YYYY', this.options.language),
  1351. 'Quarter Day_upper':
  1352. date.getDate() !== last_date.getDate()
  1353. ? date_utils.format(date, 'D MMM', this.options.language)
  1354. : '',
  1355. 'Half Day_upper':
  1356. date.getDate() !== last_date.getDate()
  1357. ? date.getMonth() !== last_date.getMonth()
  1358. ? date_utils.format(
  1359. date,
  1360. 'D MMM',
  1361. this.options.language
  1362. )
  1363. : date_utils.format(date, 'D', this.options.language)
  1364. : '',
  1365. Day_upper:
  1366. date.getMonth() !== last_date.getMonth()
  1367. ? date_utils.format(date, 'MMMM', this.options.language)
  1368. : '',
  1369. Week_upper:
  1370. date.getMonth() !== last_date.getMonth()
  1371. ? date_utils.format(date, 'MMMM', this.options.language)
  1372. : '',
  1373. Month_upper:
  1374. date.getFullYear() !== last_date.getFullYear()
  1375. ? date_utils.format(date, 'YYYY', this.options.language)
  1376. : '',
  1377. Year_upper:
  1378. date.getFullYear() !== last_date.getFullYear()
  1379. ? date_utils.format(date, 'YYYY', this.options.language)
  1380. : '',
  1381. };
  1382. const base_pos = {
  1383. x: i * this.options.column_width,
  1384. lower_y: this.options.header_height,
  1385. upper_y: this.options.header_height - 25,
  1386. };
  1387. const x_pos = {
  1388. 'Quarter Day_lower': (this.options.column_width * 4) / 2,
  1389. 'Quarter Day_upper': 0,
  1390. 'Half Day_lower': (this.options.column_width * 2) / 2,
  1391. 'Half Day_upper': 0,
  1392. Day_lower: this.options.column_width / 2,
  1393. Day_upper: (this.options.column_width * 30) / 2,
  1394. Week_lower: 0,
  1395. Week_upper: (this.options.column_width * 4) / 2,
  1396. Month_lower: this.options.column_width / 2,
  1397. Month_upper: (this.options.column_width * 12) / 2,
  1398. Year_lower: this.options.column_width / 2,
  1399. Year_upper: (this.options.column_width * 30) / 2,
  1400. };
  1401. return {
  1402. upper_text: date_text[`${this.options.view_mode}_upper`],
  1403. lower_text: date_text[`${this.options.view_mode}_lower`],
  1404. upper_x: base_pos.x + x_pos[`${this.options.view_mode}_upper`],
  1405. upper_y: base_pos.upper_y,
  1406. lower_x: base_pos.x + x_pos[`${this.options.view_mode}_lower`],
  1407. lower_y: base_pos.lower_y,
  1408. };
  1409. }
  1410. make_bars() {
  1411. this.bars = this.tasks.map((task) => {
  1412. const bar = new Bar(this, task);
  1413. this.layers.bar.appendChild(bar.group);
  1414. return bar;
  1415. });
  1416. }
  1417. make_arrows() {
  1418. this.arrows = [];
  1419. for (let task of this.tasks) {
  1420. let arrows = [];
  1421. arrows = task.dependencies
  1422. .map((task_id) => {
  1423. const dependency = this.get_task(task_id);
  1424. if (!dependency) return;
  1425. const arrow = new Arrow(
  1426. this,
  1427. this.bars[dependency._index], // from_task
  1428. this.bars[task._index] // to_task
  1429. );
  1430. this.layers.arrow.appendChild(arrow.element);
  1431. return arrow;
  1432. })
  1433. .filter(Boolean); // filter falsy values
  1434. this.arrows = this.arrows.concat(arrows);
  1435. }
  1436. }
  1437. map_arrows_on_bars() {
  1438. for (let bar of this.bars) {
  1439. bar.arrows = this.arrows.filter((arrow) => {
  1440. return (
  1441. arrow.from_task.task.id === bar.task.id ||
  1442. arrow.to_task.task.id === bar.task.id
  1443. );
  1444. });
  1445. }
  1446. }
  1447. set_width() {
  1448. const cur_width = this.$svg.getBoundingClientRect().width;
  1449. const actual_width = this.$svg
  1450. .querySelector('.grid .grid-row')
  1451. .getAttribute('width');
  1452. if (cur_width < actual_width) {
  1453. this.$svg.setAttribute('width', actual_width);
  1454. }
  1455. }
  1456. set_scroll_position() {
  1457. const parent_element = this.$svg.parentElement;
  1458. if (!parent_element) return;
  1459. const hours_before_first_task = date_utils.diff(
  1460. this.get_oldest_starting_date(),
  1461. this.gantt_start,
  1462. 'hour'
  1463. );
  1464. const scroll_pos =
  1465. (hours_before_first_task / this.options.step) *
  1466. this.options.column_width -
  1467. this.options.column_width;
  1468. parent_element.scrollLeft = scroll_pos;
  1469. }
  1470. bind_grid_click() {
  1471. $.on(
  1472. this.$svg,
  1473. this.options.popup_trigger,
  1474. '.grid-row, .grid-header',
  1475. () => {
  1476. this.unselect_all();
  1477. this.hide_popup();
  1478. }
  1479. );
  1480. }
  1481. bind_bar_events() {
  1482. let is_dragging = false;
  1483. let x_on_start = 0;
  1484. let y_on_start = 0;
  1485. let is_resizing_left = false;
  1486. let is_resizing_right = false;
  1487. let parent_bar_id = null;
  1488. let bars = []; // instanceof Bar
  1489. this.bar_being_dragged = null;
  1490. function action_in_progress() {
  1491. return is_dragging || is_resizing_left || is_resizing_right;
  1492. }
  1493. $.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => {
  1494. const bar_wrapper = $.closest('.bar-wrapper', element);
  1495. if (element.classList.contains('left')) {
  1496. is_resizing_left = true;
  1497. } else if (element.classList.contains('right')) {
  1498. is_resizing_right = true;
  1499. } else if (element.classList.contains('bar-wrapper')) {
  1500. is_dragging = true;
  1501. }
  1502. bar_wrapper.classList.add('active');
  1503. x_on_start = e.offsetX;
  1504. y_on_start = e.offsetY;
  1505. parent_bar_id = bar_wrapper.getAttribute('data-id');
  1506. const ids = [
  1507. parent_bar_id,
  1508. ...this.get_all_dependent_tasks(parent_bar_id),
  1509. ];
  1510. bars = ids.map((id) => this.get_bar(id));
  1511. this.bar_being_dragged = parent_bar_id;
  1512. bars.forEach((bar) => {
  1513. const $bar = bar.$bar;
  1514. $bar.ox = $bar.getX();
  1515. $bar.oy = $bar.getY();
  1516. $bar.owidth = $bar.getWidth();
  1517. $bar.finaldx = 0;
  1518. });
  1519. });
  1520. $.on(this.$svg, 'mousemove', (e) => {
  1521. if (!action_in_progress()) return;
  1522. const dx = e.offsetX - x_on_start;
  1523. e.offsetY - y_on_start;
  1524. bars.forEach((bar) => {
  1525. const $bar = bar.$bar;
  1526. $bar.finaldx = this.get_snap_position(dx);
  1527. this.hide_popup();
  1528. if (is_resizing_left) {
  1529. if (parent_bar_id === bar.task.id) {
  1530. bar.update_bar_position({
  1531. x: $bar.ox + $bar.finaldx,
  1532. width: $bar.owidth - $bar.finaldx,
  1533. });
  1534. } else {
  1535. bar.update_bar_position({
  1536. x: $bar.ox + $bar.finaldx,
  1537. });
  1538. }
  1539. } else if (is_resizing_right) {
  1540. if (parent_bar_id === bar.task.id) {
  1541. bar.update_bar_position({
  1542. width: $bar.owidth + $bar.finaldx,
  1543. });
  1544. }
  1545. } else if (is_dragging) {
  1546. bar.update_bar_position({ x: $bar.ox + $bar.finaldx });
  1547. }
  1548. });
  1549. });
  1550. document.addEventListener('mouseup', (e) => {
  1551. if (is_dragging || is_resizing_left || is_resizing_right) {
  1552. bars.forEach((bar) => bar.group.classList.remove('active'));
  1553. }
  1554. is_dragging = false;
  1555. is_resizing_left = false;
  1556. is_resizing_right = false;
  1557. });
  1558. $.on(this.$svg, 'mouseup', (e) => {
  1559. this.bar_being_dragged = null;
  1560. bars.forEach((bar) => {
  1561. const $bar = bar.$bar;
  1562. if (!$bar.finaldx) return;
  1563. bar.date_changed();
  1564. bar.set_action_completed();
  1565. });
  1566. });
  1567. this.bind_bar_progress();
  1568. }
  1569. bind_bar_progress() {
  1570. let x_on_start = 0;
  1571. let y_on_start = 0;
  1572. let is_resizing = null;
  1573. let bar = null;
  1574. let $bar_progress = null;
  1575. let $bar = null;
  1576. $.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => {
  1577. is_resizing = true;
  1578. x_on_start = e.offsetX;
  1579. y_on_start = e.offsetY;
  1580. const $bar_wrapper = $.closest('.bar-wrapper', handle);
  1581. const id = $bar_wrapper.getAttribute('data-id');
  1582. bar = this.get_bar(id);
  1583. $bar_progress = bar.$bar_progress;
  1584. $bar = bar.$bar;
  1585. $bar_progress.finaldx = 0;
  1586. $bar_progress.owidth = $bar_progress.getWidth();
  1587. $bar_progress.min_dx = -$bar_progress.getWidth();
  1588. $bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth();
  1589. });
  1590. $.on(this.$svg, 'mousemove', (e) => {
  1591. if (!is_resizing) return;
  1592. let dx = e.offsetX - x_on_start;
  1593. e.offsetY - y_on_start;
  1594. if (dx > $bar_progress.max_dx) {
  1595. dx = $bar_progress.max_dx;
  1596. }
  1597. if (dx < $bar_progress.min_dx) {
  1598. dx = $bar_progress.min_dx;
  1599. }
  1600. const $handle = bar.$handle_progress;
  1601. $.attr($bar_progress, 'width', $bar_progress.owidth + dx);
  1602. $.attr($handle, 'points', bar.get_progress_polygon_points());
  1603. $bar_progress.finaldx = dx;
  1604. });
  1605. $.on(this.$svg, 'mouseup', () => {
  1606. is_resizing = false;
  1607. if (!($bar_progress && $bar_progress.finaldx)) return;
  1608. bar.progress_changed();
  1609. bar.set_action_completed();
  1610. });
  1611. }
  1612. get_all_dependent_tasks(task_id) {
  1613. let out = [];
  1614. let to_process = [task_id];
  1615. while (to_process.length) {
  1616. const deps = to_process.reduce((acc, curr) => {
  1617. acc = acc.concat(this.dependency_map[curr]);
  1618. return acc;
  1619. }, []);
  1620. out = out.concat(deps);
  1621. to_process = deps.filter((d) => !to_process.includes(d));
  1622. }
  1623. return out.filter(Boolean);
  1624. }
  1625. get_snap_position(dx) {
  1626. let odx = dx,
  1627. rem,
  1628. position;
  1629. if (this.view_is(VIEW_MODE.WEEK)) {
  1630. rem = dx % (this.options.column_width / 7);
  1631. position =
  1632. odx -
  1633. rem +
  1634. (rem < this.options.column_width / 14
  1635. ? 0
  1636. : this.options.column_width / 7);
  1637. } else if (this.view_is(VIEW_MODE.MONTH)) {
  1638. rem = dx % (this.options.column_width / 30);
  1639. position =
  1640. odx -
  1641. rem +
  1642. (rem < this.options.column_width / 60
  1643. ? 0
  1644. : this.options.column_width / 30);
  1645. } else {
  1646. rem = dx % this.options.column_width;
  1647. position =
  1648. odx -
  1649. rem +
  1650. (rem < this.options.column_width / 2
  1651. ? 0
  1652. : this.options.column_width);
  1653. }
  1654. return position;
  1655. }
  1656. unselect_all() {
  1657. [...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => {
  1658. el.classList.remove('active');
  1659. });
  1660. }
  1661. view_is(modes) {
  1662. if (typeof modes === 'string') {
  1663. return this.options.view_mode === modes;
  1664. }
  1665. if (Array.isArray(modes)) {
  1666. return modes.some((mode) => this.options.view_mode === mode);
  1667. }
  1668. return false;
  1669. }
  1670. get_task(id) {
  1671. return this.tasks.find((task) => {
  1672. return task.id === id;
  1673. });
  1674. }
  1675. get_bar(id) {
  1676. return this.bars.find((bar) => {
  1677. return bar.task.id === id;
  1678. });
  1679. }
  1680. show_popup(options) {
  1681. if (!this.popup) {
  1682. this.popup = new Popup(
  1683. this.popup_wrapper,
  1684. this.options.custom_popup_html
  1685. );
  1686. }
  1687. this.popup.show(options);
  1688. }
  1689. hide_popup() {
  1690. this.popup && this.popup.hide();
  1691. }
  1692. trigger_event(event, args) {
  1693. if (this.options['on_' + event]) {
  1694. this.options['on_' + event].apply(null, args);
  1695. }
  1696. }
  1697. /**
  1698. * Gets the oldest starting date from the list of tasks
  1699. *
  1700. * @returns Date
  1701. * @memberof Gantt
  1702. */
  1703. get_oldest_starting_date() {
  1704. return this.tasks
  1705. .map((task) => task._start)
  1706. .reduce((prev_date, cur_date) =>
  1707. cur_date <= prev_date ? cur_date : prev_date
  1708. );
  1709. }
  1710. /**
  1711. * Clear all elements from the parent svg element
  1712. *
  1713. * @memberof Gantt
  1714. */
  1715. clear() {
  1716. this.$svg.innerHTML = '';
  1717. }
  1718. }
  1719. Gantt.VIEW_MODE = VIEW_MODE;
  1720. function generate_id(task) {
  1721. return task.name + '_' + Math.random().toString(36).slice(2, 12);
  1722. }
  1723. return Gantt;
  1724. })();
  1725. //# sourceMappingURL=xhiveframework-gantt.js.map