Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

pirms 2 gadiem
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  1. import date_utils from './date_utils';
  2. import { $, createSVG } from './svg_utils';
  3. import Bar from './bar';
  4. import Arrow from './arrow';
  5. import Popup from './popup';
  6. import './gantt.scss';
  7. const VIEW_MODE = {
  8. QUARTER_DAY: 'Quarter Day',
  9. HALF_DAY: 'Half Day',
  10. DAY: 'Day',
  11. WEEK: 'Week',
  12. MONTH: 'Month',
  13. YEAR: 'Year',
  14. };
  15. export default class Gantt {
  16. constructor(wrapper, tasks, options) {
  17. this.setup_wrapper(wrapper);
  18. this.setup_options(options);
  19. this.setup_tasks(tasks);
  20. // initialize with default view mode
  21. this.change_view_mode();
  22. this.bind_events();
  23. }
  24. setup_wrapper(element) {
  25. let svg_element, wrapper_element;
  26. // CSS Selector is passed
  27. if (typeof element === 'string') {
  28. element = document.querySelector(element);
  29. }
  30. // get the SVGElement
  31. if (element instanceof HTMLElement) {
  32. wrapper_element = element;
  33. svg_element = element.querySelector('svg');
  34. } else if (element instanceof SVGElement) {
  35. svg_element = element;
  36. } else {
  37. throw new TypeError(
  38. 'XhiveFramework Gantt only supports usage of a string CSS selector,' +
  39. " HTML DOM element or SVG DOM element for the 'element' parameter"
  40. );
  41. }
  42. // svg element
  43. if (!svg_element) {
  44. // create it
  45. this.$svg = createSVG('svg', {
  46. append_to: wrapper_element,
  47. class: 'gantt',
  48. });
  49. } else {
  50. this.$svg = svg_element;
  51. this.$svg.classList.add('gantt');
  52. }
  53. // wrapper element
  54. this.$container = document.createElement('div');
  55. this.$container.classList.add('gantt-container');
  56. const parent_element = this.$svg.parentElement;
  57. parent_element.appendChild(this.$container);
  58. this.$container.appendChild(this.$svg);
  59. // popup wrapper
  60. this.popup_wrapper = document.createElement('div');
  61. this.popup_wrapper.classList.add('popup-wrapper');
  62. this.$container.appendChild(this.popup_wrapper);
  63. }
  64. setup_options(options) {
  65. const default_options = {
  66. header_height: 50,
  67. column_width: 30,
  68. step: 24,
  69. view_modes: [...Object.values(VIEW_MODE)],
  70. bar_height: 20,
  71. bar_corner_radius: 3,
  72. arrow_curve: 5,
  73. padding: 18,
  74. view_mode: 'Day',
  75. date_format: 'YYYY-MM-DD',
  76. popup_trigger: 'click',
  77. custom_popup_html: null,
  78. language: 'en',
  79. };
  80. this.options = Object.assign({}, default_options, options);
  81. }
  82. setup_tasks(tasks) {
  83. // prepare tasks
  84. this.tasks = tasks.map((task, i) => {
  85. // convert to Date objects
  86. task._start = date_utils.parse(task.start);
  87. task._end = date_utils.parse(task.end);
  88. // make task invalid if duration too large
  89. if (date_utils.diff(task._end, task._start, 'year') > 10) {
  90. task.end = null;
  91. }
  92. // cache index
  93. task._index = i;
  94. // invalid dates
  95. if (!task.start && !task.end) {
  96. const today = date_utils.today();
  97. task._start = today;
  98. task._end = date_utils.add(today, 2, 'day');
  99. }
  100. if (!task.start && task.end) {
  101. task._start = date_utils.add(task._end, -2, 'day');
  102. }
  103. if (task.start && !task.end) {
  104. task._end = date_utils.add(task._start, 2, 'day');
  105. }
  106. // if hours is not set, assume the last day is full day
  107. // e.g: 2018-09-09 becomes 2018-09-09 23:59:59
  108. const task_end_values = date_utils.get_date_values(task._end);
  109. if (task_end_values.slice(3).every((d) => d === 0)) {
  110. task._end = date_utils.add(task._end, 24, 'hour');
  111. }
  112. // invalid flag
  113. if (!task.start || !task.end) {
  114. task.invalid = true;
  115. }
  116. // dependencies
  117. if (typeof task.dependencies === 'string' || !task.dependencies) {
  118. let deps = [];
  119. if (task.dependencies) {
  120. deps = task.dependencies
  121. .split(',')
  122. .map((d) => d.trim())
  123. .filter((d) => d);
  124. }
  125. task.dependencies = deps;
  126. }
  127. // uids
  128. if (!task.id) {
  129. task.id = generate_id(task);
  130. }
  131. return task;
  132. });
  133. this.setup_dependencies();
  134. }
  135. setup_dependencies() {
  136. this.dependency_map = {};
  137. for (let t of this.tasks) {
  138. for (let d of t.dependencies) {
  139. this.dependency_map[d] = this.dependency_map[d] || [];
  140. this.dependency_map[d].push(t.id);
  141. }
  142. }
  143. }
  144. refresh(tasks) {
  145. this.setup_tasks(tasks);
  146. this.change_view_mode();
  147. }
  148. change_view_mode(mode = this.options.view_mode) {
  149. this.update_view_scale(mode);
  150. this.setup_dates();
  151. this.render();
  152. // fire viewmode_change event
  153. this.trigger_event('view_change', [mode]);
  154. }
  155. update_view_scale(view_mode) {
  156. this.options.view_mode = view_mode;
  157. if (view_mode === VIEW_MODE.DAY) {
  158. this.options.step = 24;
  159. this.options.column_width = 38;
  160. } else if (view_mode === VIEW_MODE.HALF_DAY) {
  161. this.options.step = 24 / 2;
  162. this.options.column_width = 38;
  163. } else if (view_mode === VIEW_MODE.QUARTER_DAY) {
  164. this.options.step = 24 / 4;
  165. this.options.column_width = 38;
  166. } else if (view_mode === VIEW_MODE.WEEK) {
  167. this.options.step = 24 * 7;
  168. this.options.column_width = 140;
  169. } else if (view_mode === VIEW_MODE.MONTH) {
  170. this.options.step = 24 * 30;
  171. this.options.column_width = 120;
  172. } else if (view_mode === VIEW_MODE.YEAR) {
  173. this.options.step = 24 * 365;
  174. this.options.column_width = 120;
  175. }
  176. }
  177. setup_dates() {
  178. this.setup_gantt_dates();
  179. this.setup_date_values();
  180. }
  181. setup_gantt_dates() {
  182. this.gantt_start = this.gantt_end = null;
  183. for (let task of this.tasks) {
  184. // set global start and end date
  185. if (!this.gantt_start || task._start < this.gantt_start) {
  186. this.gantt_start = task._start;
  187. }
  188. if (!this.gantt_end || task._end > this.gantt_end) {
  189. this.gantt_end = task._end;
  190. }
  191. }
  192. this.gantt_start = date_utils.start_of(this.gantt_start, 'day');
  193. this.gantt_end = date_utils.start_of(this.gantt_end, 'day');
  194. // add date padding on both sides
  195. if (this.view_is([VIEW_MODE.QUARTER_DAY, VIEW_MODE.HALF_DAY])) {
  196. this.gantt_start = date_utils.add(this.gantt_start, -7, 'day');
  197. this.gantt_end = date_utils.add(this.gantt_end, 7, 'day');
  198. } else if (this.view_is(VIEW_MODE.MONTH)) {
  199. this.gantt_start = date_utils.start_of(this.gantt_start, 'year');
  200. this.gantt_end = date_utils.add(this.gantt_end, 1, 'year');
  201. } else if (this.view_is(VIEW_MODE.YEAR)) {
  202. this.gantt_start = date_utils.add(this.gantt_start, -2, 'year');
  203. this.gantt_end = date_utils.add(this.gantt_end, 2, 'year');
  204. } else {
  205. this.gantt_start = date_utils.add(this.gantt_start, -1, 'month');
  206. this.gantt_end = date_utils.add(this.gantt_end, 1, 'month');
  207. }
  208. }
  209. setup_date_values() {
  210. this.dates = [];
  211. let cur_date = null;
  212. while (cur_date === null || cur_date < this.gantt_end) {
  213. if (!cur_date) {
  214. cur_date = date_utils.clone(this.gantt_start);
  215. } else {
  216. if (this.view_is(VIEW_MODE.YEAR)) {
  217. cur_date = date_utils.add(cur_date, 1, 'year');
  218. } else if (this.view_is(VIEW_MODE.MONTH)) {
  219. cur_date = date_utils.add(cur_date, 1, 'month');
  220. } else {
  221. cur_date = date_utils.add(
  222. cur_date,
  223. this.options.step,
  224. 'hour'
  225. );
  226. }
  227. }
  228. this.dates.push(cur_date);
  229. }
  230. }
  231. bind_events() {
  232. this.bind_grid_click();
  233. this.bind_bar_events();
  234. }
  235. render() {
  236. this.clear();
  237. this.setup_layers();
  238. this.make_grid();
  239. this.make_dates();
  240. this.make_bars();
  241. this.make_arrows();
  242. this.map_arrows_on_bars();
  243. this.set_width();
  244. this.set_scroll_position();
  245. }
  246. setup_layers() {
  247. this.layers = {};
  248. const layers = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];
  249. // make group layers
  250. for (let layer of layers) {
  251. this.layers[layer] = createSVG('g', {
  252. class: layer,
  253. append_to: this.$svg,
  254. });
  255. }
  256. }
  257. make_grid() {
  258. this.make_grid_background();
  259. this.make_grid_rows();
  260. this.make_grid_header();
  261. this.make_grid_ticks();
  262. this.make_grid_highlights();
  263. }
  264. make_grid_background() {
  265. const grid_width = this.dates.length * this.options.column_width;
  266. const grid_height =
  267. this.options.header_height +
  268. this.options.padding +
  269. (this.options.bar_height + this.options.padding) *
  270. this.tasks.length;
  271. createSVG('rect', {
  272. x: 0,
  273. y: 0,
  274. width: grid_width,
  275. height: grid_height,
  276. class: 'grid-background',
  277. append_to: this.layers.grid,
  278. });
  279. $.attr(this.$svg, {
  280. height: grid_height + this.options.padding + 100,
  281. width: '100%',
  282. });
  283. }
  284. make_grid_rows() {
  285. const rows_layer = createSVG('g', { append_to: this.layers.grid });
  286. const lines_layer = createSVG('g', { append_to: this.layers.grid });
  287. const row_width = this.dates.length * this.options.column_width;
  288. const row_height = this.options.bar_height + this.options.padding;
  289. let row_y = this.options.header_height + this.options.padding / 2;
  290. for (let task of this.tasks) {
  291. createSVG('rect', {
  292. x: 0,
  293. y: row_y,
  294. width: row_width,
  295. height: row_height,
  296. class: 'grid-row',
  297. append_to: rows_layer,
  298. });
  299. createSVG('line', {
  300. x1: 0,
  301. y1: row_y + row_height,
  302. x2: row_width,
  303. y2: row_y + row_height,
  304. class: 'row-line',
  305. append_to: lines_layer,
  306. });
  307. row_y += this.options.bar_height + this.options.padding;
  308. }
  309. }
  310. make_grid_header() {
  311. const header_width = this.dates.length * this.options.column_width;
  312. const header_height = this.options.header_height + 10;
  313. createSVG('rect', {
  314. x: 0,
  315. y: 0,
  316. width: header_width,
  317. height: header_height,
  318. class: 'grid-header',
  319. append_to: this.layers.grid,
  320. });
  321. }
  322. make_grid_ticks() {
  323. let tick_x = 0;
  324. let tick_y = this.options.header_height + this.options.padding / 2;
  325. let tick_height =
  326. (this.options.bar_height + this.options.padding) *
  327. this.tasks.length;
  328. for (let date of this.dates) {
  329. let tick_class = 'tick';
  330. // thick tick for monday
  331. if (this.view_is(VIEW_MODE.DAY) && date.getDate() === 1) {
  332. tick_class += ' thick';
  333. }
  334. // thick tick for first week
  335. if (
  336. this.view_is(VIEW_MODE.WEEK) &&
  337. date.getDate() >= 1 &&
  338. date.getDate() < 8
  339. ) {
  340. tick_class += ' thick';
  341. }
  342. // thick ticks for quarters
  343. if (
  344. this.view_is(VIEW_MODE.MONTH) &&
  345. (date.getMonth() + 1) % 3 === 0
  346. ) {
  347. tick_class += ' thick';
  348. }
  349. createSVG('path', {
  350. d: `M ${tick_x} ${tick_y} v ${tick_height}`,
  351. class: tick_class,
  352. append_to: this.layers.grid,
  353. });
  354. if (this.view_is(VIEW_MODE.MONTH)) {
  355. tick_x +=
  356. (date_utils.get_days_in_month(date) *
  357. this.options.column_width) /
  358. 30;
  359. } else {
  360. tick_x += this.options.column_width;
  361. }
  362. }
  363. }
  364. make_grid_highlights() {
  365. // highlight today's date
  366. if (this.view_is(VIEW_MODE.DAY)) {
  367. const x =
  368. (date_utils.diff(date_utils.today(), this.gantt_start, 'hour') /
  369. this.options.step) *
  370. this.options.column_width;
  371. const y = 0;
  372. const width = this.options.column_width;
  373. const height =
  374. (this.options.bar_height + this.options.padding) *
  375. this.tasks.length +
  376. this.options.header_height +
  377. this.options.padding / 2;
  378. createSVG('rect', {
  379. x,
  380. y,
  381. width,
  382. height,
  383. class: 'today-highlight',
  384. append_to: this.layers.grid,
  385. });
  386. }
  387. }
  388. make_dates() {
  389. for (let date of this.get_dates_to_draw()) {
  390. createSVG('text', {
  391. x: date.lower_x,
  392. y: date.lower_y,
  393. innerHTML: date.lower_text,
  394. class: 'lower-text',
  395. append_to: this.layers.date,
  396. });
  397. if (date.upper_text) {
  398. const $upper_text = createSVG('text', {
  399. x: date.upper_x,
  400. y: date.upper_y,
  401. innerHTML: date.upper_text,
  402. class: 'upper-text',
  403. append_to: this.layers.date,
  404. });
  405. // remove out-of-bound dates
  406. if (
  407. $upper_text.getBBox().x2 > this.layers.grid.getBBox().width
  408. ) {
  409. $upper_text.remove();
  410. }
  411. }
  412. }
  413. }
  414. get_dates_to_draw() {
  415. let last_date = null;
  416. const dates = this.dates.map((date, i) => {
  417. const d = this.get_date_info(date, last_date, i);
  418. last_date = date;
  419. return d;
  420. });
  421. return dates;
  422. }
  423. get_date_info(date, last_date, i) {
  424. if (!last_date) {
  425. last_date = date_utils.add(date, 1, 'year');
  426. }
  427. const date_text = {
  428. 'Quarter Day_lower': date_utils.format(
  429. date,
  430. 'HH',
  431. this.options.language
  432. ),
  433. 'Half Day_lower': date_utils.format(
  434. date,
  435. 'HH',
  436. this.options.language
  437. ),
  438. Day_lower:
  439. date.getDate() !== last_date.getDate()
  440. ? date_utils.format(date, 'D', this.options.language)
  441. : '',
  442. Week_lower:
  443. date.getMonth() !== last_date.getMonth()
  444. ? date_utils.format(date, 'D MMM', this.options.language)
  445. : date_utils.format(date, 'D', this.options.language),
  446. Month_lower: date_utils.format(date, 'MMMM', this.options.language),
  447. Year_lower: date_utils.format(date, 'YYYY', this.options.language),
  448. 'Quarter Day_upper':
  449. date.getDate() !== last_date.getDate()
  450. ? date_utils.format(date, 'D MMM', this.options.language)
  451. : '',
  452. 'Half Day_upper':
  453. date.getDate() !== last_date.getDate()
  454. ? date.getMonth() !== last_date.getMonth()
  455. ? date_utils.format(
  456. date,
  457. 'D MMM',
  458. this.options.language
  459. )
  460. : date_utils.format(date, 'D', this.options.language)
  461. : '',
  462. Day_upper:
  463. date.getMonth() !== last_date.getMonth()
  464. ? date_utils.format(date, 'MMMM', this.options.language)
  465. : '',
  466. Week_upper:
  467. date.getMonth() !== last_date.getMonth()
  468. ? date_utils.format(date, 'MMMM', this.options.language)
  469. : '',
  470. Month_upper:
  471. date.getFullYear() !== last_date.getFullYear()
  472. ? date_utils.format(date, 'YYYY', this.options.language)
  473. : '',
  474. Year_upper:
  475. date.getFullYear() !== last_date.getFullYear()
  476. ? date_utils.format(date, 'YYYY', this.options.language)
  477. : '',
  478. };
  479. const base_pos = {
  480. x: i * this.options.column_width,
  481. lower_y: this.options.header_height,
  482. upper_y: this.options.header_height - 25,
  483. };
  484. const x_pos = {
  485. 'Quarter Day_lower': (this.options.column_width * 4) / 2,
  486. 'Quarter Day_upper': 0,
  487. 'Half Day_lower': (this.options.column_width * 2) / 2,
  488. 'Half Day_upper': 0,
  489. Day_lower: this.options.column_width / 2,
  490. Day_upper: (this.options.column_width * 30) / 2,
  491. Week_lower: 0,
  492. Week_upper: (this.options.column_width * 4) / 2,
  493. Month_lower: this.options.column_width / 2,
  494. Month_upper: (this.options.column_width * 12) / 2,
  495. Year_lower: this.options.column_width / 2,
  496. Year_upper: (this.options.column_width * 30) / 2,
  497. };
  498. return {
  499. upper_text: date_text[`${this.options.view_mode}_upper`],
  500. lower_text: date_text[`${this.options.view_mode}_lower`],
  501. upper_x: base_pos.x + x_pos[`${this.options.view_mode}_upper`],
  502. upper_y: base_pos.upper_y,
  503. lower_x: base_pos.x + x_pos[`${this.options.view_mode}_lower`],
  504. lower_y: base_pos.lower_y,
  505. };
  506. }
  507. make_bars() {
  508. this.bars = this.tasks.map((task) => {
  509. const bar = new Bar(this, task);
  510. this.layers.bar.appendChild(bar.group);
  511. return bar;
  512. });
  513. }
  514. make_arrows() {
  515. this.arrows = [];
  516. for (let task of this.tasks) {
  517. let arrows = [];
  518. arrows = task.dependencies
  519. .map((task_id) => {
  520. const dependency = this.get_task(task_id);
  521. if (!dependency) return;
  522. const arrow = new Arrow(
  523. this,
  524. this.bars[dependency._index], // from_task
  525. this.bars[task._index] // to_task
  526. );
  527. this.layers.arrow.appendChild(arrow.element);
  528. return arrow;
  529. })
  530. .filter(Boolean); // filter falsy values
  531. this.arrows = this.arrows.concat(arrows);
  532. }
  533. }
  534. map_arrows_on_bars() {
  535. for (let bar of this.bars) {
  536. bar.arrows = this.arrows.filter((arrow) => {
  537. return (
  538. arrow.from_task.task.id === bar.task.id ||
  539. arrow.to_task.task.id === bar.task.id
  540. );
  541. });
  542. }
  543. }
  544. set_width() {
  545. const cur_width = this.$svg.getBoundingClientRect().width;
  546. const actual_width = this.$svg
  547. .querySelector('.grid .grid-row')
  548. .getAttribute('width');
  549. if (cur_width < actual_width) {
  550. this.$svg.setAttribute('width', actual_width);
  551. }
  552. }
  553. set_scroll_position() {
  554. const parent_element = this.$svg.parentElement;
  555. if (!parent_element) return;
  556. const hours_before_first_task = date_utils.diff(
  557. this.get_oldest_starting_date(),
  558. this.gantt_start,
  559. 'hour'
  560. );
  561. const scroll_pos =
  562. (hours_before_first_task / this.options.step) *
  563. this.options.column_width -
  564. this.options.column_width;
  565. parent_element.scrollLeft = scroll_pos;
  566. }
  567. bind_grid_click() {
  568. $.on(
  569. this.$svg,
  570. this.options.popup_trigger,
  571. '.grid-row, .grid-header',
  572. () => {
  573. this.unselect_all();
  574. this.hide_popup();
  575. }
  576. );
  577. }
  578. bind_bar_events() {
  579. let is_dragging = false;
  580. let x_on_start = 0;
  581. let y_on_start = 0;
  582. let is_resizing_left = false;
  583. let is_resizing_right = false;
  584. let parent_bar_id = null;
  585. let bars = []; // instanceof Bar
  586. this.bar_being_dragged = null;
  587. function action_in_progress() {
  588. return is_dragging || is_resizing_left || is_resizing_right;
  589. }
  590. $.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => {
  591. const bar_wrapper = $.closest('.bar-wrapper', element);
  592. if (element.classList.contains('left')) {
  593. is_resizing_left = true;
  594. } else if (element.classList.contains('right')) {
  595. is_resizing_right = true;
  596. } else if (element.classList.contains('bar-wrapper')) {
  597. is_dragging = true;
  598. }
  599. bar_wrapper.classList.add('active');
  600. x_on_start = e.offsetX;
  601. y_on_start = e.offsetY;
  602. parent_bar_id = bar_wrapper.getAttribute('data-id');
  603. const ids = [
  604. parent_bar_id,
  605. ...this.get_all_dependent_tasks(parent_bar_id),
  606. ];
  607. bars = ids.map((id) => this.get_bar(id));
  608. this.bar_being_dragged = parent_bar_id;
  609. bars.forEach((bar) => {
  610. const $bar = bar.$bar;
  611. $bar.ox = $bar.getX();
  612. $bar.oy = $bar.getY();
  613. $bar.owidth = $bar.getWidth();
  614. $bar.finaldx = 0;
  615. });
  616. });
  617. $.on(this.$svg, 'mousemove', (e) => {
  618. if (!action_in_progress()) return;
  619. const dx = e.offsetX - x_on_start;
  620. const dy = e.offsetY - y_on_start;
  621. bars.forEach((bar) => {
  622. const $bar = bar.$bar;
  623. $bar.finaldx = this.get_snap_position(dx);
  624. this.hide_popup();
  625. if (is_resizing_left) {
  626. if (parent_bar_id === bar.task.id) {
  627. bar.update_bar_position({
  628. x: $bar.ox + $bar.finaldx,
  629. width: $bar.owidth - $bar.finaldx,
  630. });
  631. } else {
  632. bar.update_bar_position({
  633. x: $bar.ox + $bar.finaldx,
  634. });
  635. }
  636. } else if (is_resizing_right) {
  637. if (parent_bar_id === bar.task.id) {
  638. bar.update_bar_position({
  639. width: $bar.owidth + $bar.finaldx,
  640. });
  641. }
  642. } else if (is_dragging) {
  643. bar.update_bar_position({ x: $bar.ox + $bar.finaldx });
  644. }
  645. });
  646. });
  647. document.addEventListener('mouseup', (e) => {
  648. if (is_dragging || is_resizing_left || is_resizing_right) {
  649. bars.forEach((bar) => bar.group.classList.remove('active'));
  650. }
  651. is_dragging = false;
  652. is_resizing_left = false;
  653. is_resizing_right = false;
  654. });
  655. $.on(this.$svg, 'mouseup', (e) => {
  656. this.bar_being_dragged = null;
  657. bars.forEach((bar) => {
  658. const $bar = bar.$bar;
  659. if (!$bar.finaldx) return;
  660. bar.date_changed();
  661. bar.set_action_completed();
  662. });
  663. });
  664. this.bind_bar_progress();
  665. }
  666. bind_bar_progress() {
  667. let x_on_start = 0;
  668. let y_on_start = 0;
  669. let is_resizing = null;
  670. let bar = null;
  671. let $bar_progress = null;
  672. let $bar = null;
  673. $.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => {
  674. is_resizing = true;
  675. x_on_start = e.offsetX;
  676. y_on_start = e.offsetY;
  677. const $bar_wrapper = $.closest('.bar-wrapper', handle);
  678. const id = $bar_wrapper.getAttribute('data-id');
  679. bar = this.get_bar(id);
  680. $bar_progress = bar.$bar_progress;
  681. $bar = bar.$bar;
  682. $bar_progress.finaldx = 0;
  683. $bar_progress.owidth = $bar_progress.getWidth();
  684. $bar_progress.min_dx = -$bar_progress.getWidth();
  685. $bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth();
  686. });
  687. $.on(this.$svg, 'mousemove', (e) => {
  688. if (!is_resizing) return;
  689. let dx = e.offsetX - x_on_start;
  690. let dy = e.offsetY - y_on_start;
  691. if (dx > $bar_progress.max_dx) {
  692. dx = $bar_progress.max_dx;
  693. }
  694. if (dx < $bar_progress.min_dx) {
  695. dx = $bar_progress.min_dx;
  696. }
  697. const $handle = bar.$handle_progress;
  698. $.attr($bar_progress, 'width', $bar_progress.owidth + dx);
  699. $.attr($handle, 'points', bar.get_progress_polygon_points());
  700. $bar_progress.finaldx = dx;
  701. });
  702. $.on(this.$svg, 'mouseup', () => {
  703. is_resizing = false;
  704. if (!($bar_progress && $bar_progress.finaldx)) return;
  705. bar.progress_changed();
  706. bar.set_action_completed();
  707. });
  708. }
  709. get_all_dependent_tasks(task_id) {
  710. let out = [];
  711. let to_process = [task_id];
  712. while (to_process.length) {
  713. const deps = to_process.reduce((acc, curr) => {
  714. acc = acc.concat(this.dependency_map[curr]);
  715. return acc;
  716. }, []);
  717. out = out.concat(deps);
  718. to_process = deps.filter((d) => !to_process.includes(d));
  719. }
  720. return out.filter(Boolean);
  721. }
  722. get_snap_position(dx) {
  723. let odx = dx,
  724. rem,
  725. position;
  726. if (this.view_is(VIEW_MODE.WEEK)) {
  727. rem = dx % (this.options.column_width / 7);
  728. position =
  729. odx -
  730. rem +
  731. (rem < this.options.column_width / 14
  732. ? 0
  733. : this.options.column_width / 7);
  734. } else if (this.view_is(VIEW_MODE.MONTH)) {
  735. rem = dx % (this.options.column_width / 30);
  736. position =
  737. odx -
  738. rem +
  739. (rem < this.options.column_width / 60
  740. ? 0
  741. : this.options.column_width / 30);
  742. } else {
  743. rem = dx % this.options.column_width;
  744. position =
  745. odx -
  746. rem +
  747. (rem < this.options.column_width / 2
  748. ? 0
  749. : this.options.column_width);
  750. }
  751. return position;
  752. }
  753. unselect_all() {
  754. [...this.$svg.querySelectorAll('.bar-wrapper')].forEach((el) => {
  755. el.classList.remove('active');
  756. });
  757. }
  758. view_is(modes) {
  759. if (typeof modes === 'string') {
  760. return this.options.view_mode === modes;
  761. }
  762. if (Array.isArray(modes)) {
  763. return modes.some((mode) => this.options.view_mode === mode);
  764. }
  765. return false;
  766. }
  767. get_task(id) {
  768. return this.tasks.find((task) => {
  769. return task.id === id;
  770. });
  771. }
  772. get_bar(id) {
  773. return this.bars.find((bar) => {
  774. return bar.task.id === id;
  775. });
  776. }
  777. show_popup(options) {
  778. if (!this.popup) {
  779. this.popup = new Popup(
  780. this.popup_wrapper,
  781. this.options.custom_popup_html
  782. );
  783. }
  784. this.popup.show(options);
  785. }
  786. hide_popup() {
  787. this.popup && this.popup.hide();
  788. }
  789. trigger_event(event, args) {
  790. if (this.options['on_' + event]) {
  791. this.options['on_' + event].apply(null, args);
  792. }
  793. }
  794. /**
  795. * Gets the oldest starting date from the list of tasks
  796. *
  797. * @returns Date
  798. * @memberof Gantt
  799. */
  800. get_oldest_starting_date() {
  801. return this.tasks
  802. .map((task) => task._start)
  803. .reduce((prev_date, cur_date) =>
  804. cur_date <= prev_date ? cur_date : prev_date
  805. );
  806. }
  807. /**
  808. * Clear all elements from the parent svg element
  809. *
  810. * @memberof Gantt
  811. */
  812. clear() {
  813. this.$svg.innerHTML = '';
  814. }
  815. }
  816. Gantt.VIEW_MODE = VIEW_MODE;
  817. function generate_id(task) {
  818. return task.name + '_' + Math.random().toString(36).slice(2, 12);
  819. }