var Gantt = (function () {
'use strict';
const YEAR = "year";
const MONTH = "month";
const DAY = "day";
const HOUR = "hour";
const MINUTE = "minute";
const SECOND = "second";
const MILLISECOND = "millisecond";
const SHORTENED = {
January: "Jan",
February: "Feb",
March: "Mar",
April: "Apr",
May: "May",
June: "Jun",
July: "Jul",
August: "Aug",
September: "Sep",
October: "Oct",
November: "Nov",
December: "Dec"
};
var date_utils = {
parse_duration(duration) {
const regex = /([0-9])+(y|m|d|h|min|s|ms)/gm;
const matches = regex.exec(duration);
if (matches !== null) {
if (matches[2] === "y") {
return { duration: parseInt(matches[1]), scale: `year` };
} else if (matches[2] === "m") {
return { duration: parseInt(matches[1]), scale: `month` };
} else if (matches[2] === "d") {
return { duration: parseInt(matches[1]), scale: `day` };
} else if (matches[2] === "h") {
return { duration: parseInt(matches[1]), scale: `hour` };
} else if (matches[2] === "min") {
return { duration: parseInt(matches[1]), scale: `minute` };
} else if (matches[2] === "s") {
return { duration: parseInt(matches[1]), scale: `second` };
} else if (matches[2] === "ms") {
return { duration: parseInt(matches[1]), scale: `millisecond` };
}
}
},
parse(date, date_separator = "-", time_separator = /[.:]/) {
if (date instanceof Date) {
return date;
}
if (typeof date === "string") {
let date_parts, time_parts;
const parts = date.split(" ");
date_parts = parts[0]
.split(date_separator)
.map((val) => parseInt(val, 10));
time_parts = parts[1] && parts[1].split(time_separator);
// month is 0 indexed
date_parts[1] = date_parts[1] ? date_parts[1] - 1 : 0;
let vals = date_parts;
if (time_parts && time_parts.length) {
if (time_parts.length === 4) {
time_parts[3] = "0." + time_parts[3];
time_parts[3] = parseFloat(time_parts[3]) * 1000;
}
vals = vals.concat(time_parts);
}
return new Date(...vals);
}
},
to_string(date, with_time = false) {
if (!(date instanceof Date)) {
throw new TypeError("Invalid argument type");
}
const vals = this.get_date_values(date).map((val, i) => {
if (i === 1) {
// add 1 for month
val = val + 1;
}
if (i === 6) {
return padStart(val + "", 3, "0");
}
return padStart(val + "", 2, "0");
});
const date_string = `${vals[0]}-${vals[1]}-${vals[2]}`;
const time_string = `${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}`;
return date_string + (with_time ? " " + time_string : "");
},
format(date, format_string = "YYYY-MM-DD HH:mm:ss.SSS", lang = "en") {
const dateTimeFormat = new Intl.DateTimeFormat(lang, {
month: "long",
});
const month_name = dateTimeFormat.format(date);
const month_name_capitalized =
month_name.charAt(0).toUpperCase() + month_name.slice(1);
const values = this.get_date_values(date).map((d) => padStart(d, 2, 0));
const format_map = {
YYYY: values[0],
MM: padStart(+values[1] + 1, 2, 0),
DD: values[2],
HH: values[3],
mm: values[4],
ss: values[5],
SSS: values[6],
D: values[2],
MMMM: month_name_capitalized,
MMM: SHORTENED[month_name_capitalized],
};
let str = format_string;
const formatted_values = [];
Object.keys(format_map)
.sort((a, b) => b.length - a.length) // big string first
.forEach((key) => {
if (str.includes(key)) {
str = str.replaceAll(key, `$${formatted_values.length}`);
formatted_values.push(format_map[key]);
}
});
formatted_values.forEach((value, i) => {
str = str.replaceAll(`$${i}`, value);
});
return str;
},
diff(date_a, date_b, scale = DAY) {
let milliseconds, seconds, hours, minutes, days, months, years;
milliseconds = date_a - date_b;
seconds = milliseconds / 1000;
minutes = seconds / 60;
hours = minutes / 60;
days = hours / 24;
months = days / 30;
years = months / 12;
if (!scale.endsWith("s")) {
scale += "s";
}
return Math.floor(
{
milliseconds,
seconds,
minutes,
hours,
days,
months,
years,
}[scale],
);
},
today() {
const vals = this.get_date_values(new Date()).slice(0, 3);
return new Date(...vals);
},
now() {
return new Date();
},
add(date, qty, scale) {
qty = parseInt(qty, 10);
const vals = [
date.getFullYear() + (scale === YEAR ? qty : 0),
date.getMonth() + (scale === MONTH ? qty : 0),
date.getDate() + (scale === DAY ? qty : 0),
date.getHours() + (scale === HOUR ? qty : 0),
date.getMinutes() + (scale === MINUTE ? qty : 0),
date.getSeconds() + (scale === SECOND ? qty : 0),
date.getMilliseconds() + (scale === MILLISECOND ? qty : 0),
];
return new Date(...vals);
},
start_of(date, scale) {
const scores = {
[YEAR]: 6,
[MONTH]: 5,
[DAY]: 4,
[HOUR]: 3,
[MINUTE]: 2,
[SECOND]: 1,
[MILLISECOND]: 0,
};
function should_reset(_scale) {
const max_score = scores[scale];
return scores[_scale] <= max_score;
}
const vals = [
date.getFullYear(),
should_reset(YEAR) ? 0 : date.getMonth(),
should_reset(MONTH) ? 1 : date.getDate(),
should_reset(DAY) ? 0 : date.getHours(),
should_reset(HOUR) ? 0 : date.getMinutes(),
should_reset(MINUTE) ? 0 : date.getSeconds(),
should_reset(SECOND) ? 0 : date.getMilliseconds(),
];
return new Date(...vals);
},
clone(date) {
return new Date(...this.get_date_values(date));
},
get_date_values(date) {
return [
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
];
},
get_days_in_month(date) {
const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const month = date.getMonth();
if (month !== 1) {
return no_of_days[month];
}
// Feb
const year = date.getFullYear();
if ((year % 4 === 0 && year % 100 != 0) || year % 400 === 0) {
return 29;
}
return 28;
},
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
function padStart(str, targetLength, padString) {
str = str + "";
targetLength = targetLength >> 0;
padString = String(typeof padString !== "undefined" ? padString : " ");
if (str.length > targetLength) {
return String(str);
} else {
targetLength = targetLength - str.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return padString.slice(0, targetLength) + String(str);
}
}
function $(expr, con) {
return typeof expr === "string"
? (con || document).querySelector(expr)
: expr || null;
}
function createSVG(tag, attrs) {
const elem = document.createElementNS("http://www.w3.org/2000/svg", tag);
for (let attr in attrs) {
if (attr === "append_to") {
const parent = attrs.append_to;
parent.appendChild(elem);
} else if (attr === "innerHTML") {
elem.innerHTML = attrs.innerHTML;
} else if (attr === 'clipPath') {
elem.setAttribute('clip-path', 'url(#' + attrs[attr] + ')');
} else {
elem.setAttribute(attr, attrs[attr]);
}
}
return elem;
}
function animateSVG(svgElement, attr, from, to) {
const animatedSvgElement = getAnimationElement(svgElement, attr, from, to);
if (animatedSvgElement === svgElement) {
// triggered 2nd time programmatically
// trigger artificial click event
const event = document.createEvent("HTMLEvents");
event.initEvent("click", true, true);
event.eventName = "click";
animatedSvgElement.dispatchEvent(event);
}
}
function getAnimationElement(
svgElement,
attr,
from,
to,
dur = "0.4s",
begin = "0.1s",
) {
const animEl = svgElement.querySelector("animate");
if (animEl) {
$.attr(animEl, {
attributeName: attr,
from,
to,
dur,
begin: "click + " + begin, // artificial click
});
return svgElement;
}
const animateElement = createSVG("animate", {
attributeName: attr,
from,
to,
dur,
begin,
calcMode: "spline",
values: from + ";" + to,
keyTimes: "0; 1",
keySplines: cubic_bezier("ease-out"),
});
svgElement.appendChild(animateElement);
return svgElement;
}
function cubic_bezier(name) {
return {
ease: ".25 .1 .25 1",
linear: "0 0 1 1",
"ease-in": ".42 0 1 1",
"ease-out": "0 0 .58 1",
"ease-in-out": ".42 0 .58 1",
}[name];
}
$.on = (element, event, selector, callback) => {
if (!callback) {
callback = selector;
$.bind(element, event, callback);
} else {
$.delegate(element, event, selector, callback);
}
};
$.off = (element, event, handler) => {
element.removeEventListener(event, handler);
};
$.bind = (element, event, callback) => {
event.split(/\s+/).forEach(function (event) {
element.addEventListener(event, callback);
});
};
$.delegate = (element, event, selector, callback) => {
element.addEventListener(event, function (e) {
const delegatedTarget = e.target.closest(selector);
if (delegatedTarget) {
e.delegatedTarget = delegatedTarget;
callback.call(this, e, delegatedTarget);
}
});
};
$.closest = (selector, element) => {
if (!element) return null;
if (element.matches(selector)) {
return element;
}
return $.closest(selector, element.parentNode);
};
$.attr = (element, attr, value) => {
if (!value && typeof attr === "string") {
return element.getAttribute(attr);
}
if (typeof attr === "object") {
for (let key in attr) {
$.attr(element, key, attr[key]);
}
return;
}
element.setAttribute(attr, value);
};
class Bar {
constructor(gantt, task) {
this.set_defaults(gantt, task);
this.prepare();
this.draw();
this.bind();
}
set_defaults(gantt, task) {
this.action_completed = false;
this.gantt = gantt;
this.task = task;
}
prepare() {
this.prepare_values();
this.prepare_helpers();
}
prepare_values() {
this.invalid = this.task.invalid;
this.height = this.gantt.options.bar_height;
this.image_size = this.height - 5;
this.compute_x();
this.compute_y();
this.compute_duration();
this.corner_radius = this.gantt.options.bar_corner_radius;
this.width = this.gantt.options.column_width * this.duration;
this.progress_width =
this.gantt.options.column_width *
this.duration *
(this.task.progress / 100) || 0;
this.group = createSVG("g", {
class: "bar-wrapper" + (this.task.custom_class ? " " + this.task.custom_class : "") + (this.task.important ? ' important' : ''),
"data-id": this.task.id,
});
this.bar_group = createSVG("g", {
class: "bar-group",
append_to: this.group,
});
this.handle_group = createSVG("g", {
class: "handle-group",
append_to: this.group,
});
}
prepare_helpers() {
SVGElement.prototype.getX = function () {
return +this.getAttribute("x");
};
SVGElement.prototype.getY = function () {
return +this.getAttribute("y");
};
SVGElement.prototype.getWidth = function () {
return +this.getAttribute("width");
};
SVGElement.prototype.getHeight = function () {
return +this.getAttribute("height");
};
SVGElement.prototype.getEndX = function () {
return this.getX() + this.getWidth();
};
}
prepare_expected_progress_values() {
this.compute_expected_progress();
this.expected_progress_width =
this.gantt.options.column_width *
this.duration *
(this.expected_progress / 100) || 0;
}
draw() {
this.draw_bar();
this.draw_progress_bar();
if (this.gantt.options.show_expected_progress) {
this.prepare_expected_progress_values();
this.draw_expected_progress_bar();
}
this.draw_label();
this.draw_resize_handles();
if (this.task.thumbnail) {
this.draw_thumbnail();
}
}
draw_bar() {
this.$bar = createSVG("rect", {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
rx: this.corner_radius,
ry: this.corner_radius,
class: "bar",
append_to: this.bar_group,
});
animateSVG(this.$bar, "width", 0, this.width);
if (this.invalid) {
this.$bar.classList.add("bar-invalid");
}
}
draw_expected_progress_bar() {
if (this.invalid) return;
this.$expected_bar_progress = createSVG("rect", {
x: this.x,
y: this.y,
width: this.expected_progress_width,
height: this.height,
rx: this.corner_radius,
ry: this.corner_radius,
class: "bar-expected-progress",
append_to: this.bar_group,
});
animateSVG(
this.$expected_bar_progress,
"width",
0,
this.expected_progress_width,
);
}
draw_progress_bar() {
if (this.invalid) return;
this.$bar_progress = createSVG("rect", {
x: this.x,
y: this.y,
width: this.progress_width,
height: this.height,
rx: this.corner_radius,
ry: this.corner_radius,
class: "bar-progress",
append_to: this.bar_group,
});
const x = (date_utils.diff(this.task._start, this.gantt.gantt_start, 'hour') /
this.gantt.options.step) *
this.gantt.options.column_width;
let $date_highlight = document.createElement("div");
$date_highlight.id = `${this.task.id}-highlight`;
$date_highlight.classList.add('date-highlight');
$date_highlight.style.height = this.height * 0.8 + 'px';
$date_highlight.style.width = this.width + 'px';
$date_highlight.style.top = this.gantt.options.header_height - 21 + 'px';
$date_highlight.style.left = x + 'px';
this.$date_highlight = $date_highlight;
this.gantt.$lower_header.appendChild($date_highlight);
animateSVG(this.$bar_progress, "width", 0, this.progress_width);
}
draw_label() {
let x_coord = this.x + this.$bar.getWidth() / 2;
if (this.task.thumbnail) {
x_coord = this.x + this.image_size + 5;
}
createSVG("text", {
x: x_coord,
y: this.y + this.height / 2,
innerHTML: this.task.name,
class: "bar-label",
append_to: this.bar_group,
});
// labels get BBox in the next tick
requestAnimationFrame(() => this.update_label_position());
}
draw_thumbnail() {
let x_offset = 10, y_offset = 2;
let defs, clipPath;
defs = createSVG('defs', {
append_to: this.bar_group
});
createSVG('rect', {
id: 'rect_' + this.task.id,
x: this.x + x_offset,
y: this.y + y_offset,
width: this.image_size,
height: this.image_size,
rx: '15',
class: 'img_mask',
append_to: defs
});
clipPath = createSVG('clipPath', {
id: 'clip_' + this.task.id,
append_to: defs
});
createSVG('use', {
href: '#rect_' + this.task.id,
append_to: clipPath
});
createSVG('image', {
x: this.x + x_offset,
y: this.y + y_offset,
width: this.image_size,
height: this.image_size,
class: 'bar-img',
href: this.task.thumbnail,
clipPath: 'clip_' + this.task.id,
append_to: this.bar_group
});
}
draw_resize_handles() {
if (this.invalid || this.gantt.options.readonly) return;
const bar = this.$bar;
const handle_width = 8;
createSVG("rect", {
x: bar.getX() + bar.getWidth() + handle_width - 4,
y: bar.getY() + 1,
width: handle_width,
height: this.height - 2,
rx: this.corner_radius,
ry: this.corner_radius,
class: "handle right",
append_to: this.handle_group,
});
createSVG("rect", {
x: bar.getX() - handle_width - 4,
y: bar.getY() + 1,
width: handle_width,
height: this.height - 2,
rx: this.corner_radius,
ry: this.corner_radius,
class: "handle left",
append_to: this.handle_group,
});
this.$handle_progress = createSVG("polygon", {
points: this.get_progress_polygon_points().join(","),
class: "handle progress",
append_to: this.handle_group,
});
}
get_progress_polygon_points() {
const bar_progress = this.$bar_progress;
return [
bar_progress.getEndX() - 6,
bar_progress.getY() + bar_progress.getHeight() + 8,
bar_progress.getEndX() + 6,
bar_progress.getY() + bar_progress.getHeight() + 8,
bar_progress.getEndX(),
bar_progress.getY() + bar_progress.getHeight() + 0.5,
];
}
bind() {
if (this.invalid) return;
this.setup_click_event();
}
setup_click_event() {
let task_id = this.task.id;
$.on(this.group, "mouseover", (e) => {
this.gantt.trigger_event("hover", [this.task, e.screenX, e.screenY, e]);
document.querySelector(`#${task_id}-highlight`).style.display = 'block';
});
$.on(this.group, "mouseenter", (e) => this.show_popup(e.offsetX));
$.on(this.group, "mouseleave", () => document.querySelector(`#${task_id}-highlight`).style.display = 'none');
$.on(this.group, "focus " + this.gantt.options.popup_trigger, () => {
this.gantt.trigger_event("click", [this.task]);
});
$.on(this.group, "dblclick", (e) => {
if (this.action_completed) {
// just finished a move action, wait for a few seconds
return;
}
this.gantt.trigger_event("double_click", [this.task]);
});
}
show_popup(x) {
if (this.gantt.bar_being_dragged) return;
const start_date = date_utils.format(
this.task._start,
"MMM D",
this.gantt.options.language,
);
const end_date = date_utils.format(
date_utils.add(this.task._end, -1, "second"),
"MMM D",
this.gantt.options.language,
);
const subtitle = `${start_date} - ${end_date}
Progress: ${this.task.progress}`;
this.gantt.show_popup({
x,
target_element: this.$bar,
title: this.task.name,
subtitle: subtitle,
task: this.task,
});
}
update_bar_position({ x = null, width = null }) {
const bar = this.$bar;
if (x) {
// get all x values of parent task
const xs = this.task.dependencies.map((dep) => {
return this.gantt.get_bar(dep).$bar.getX();
});
// child task must not go before parent
const valid_x = xs.reduce((prev, curr) => {
return x >= curr;
}, x);
if (!valid_x) {
width = null;
return;
}
this.update_attr(bar, "x", x);
this.update_attr(this.$date_highlight, "x", x);
}
if (width) {
this.update_attr(bar, "width", width);
this.update_attr(this.$date_highlight, "width", width);
}
this.update_label_position();
this.update_handle_position();
if (this.gantt.options.show_expected_progress) {
this.date_changed();
this.compute_duration();
this.update_expected_progressbar_position();
}
this.update_progressbar_position();
this.update_arrow_position();
}
update_label_position_on_horizontal_scroll({ x, sx }) {
const container = document.querySelector('.gantt-container');
const label = this.group.querySelector('.bar-label');
const img = this.group.querySelector('.bar-img') || '';
const img_mask = this.bar_group.querySelector('.img_mask') || '';
let barWidthLimit = this.$bar.getX() + this.$bar.getWidth();
let newLabelX = label.getX() + x;
let newImgX = img && img.getX() + x || 0;
let imgWidth = img && img.getBBox().width + 7 || 7;
let labelEndX = newLabelX + label.getBBox().width + 7;
let viewportCentral = sx + container.clientWidth / 2;
if (label.classList.contains('big')) return;
if (labelEndX < barWidthLimit && x > 0 && labelEndX < viewportCentral) {
label.setAttribute('x', newLabelX);
if (img) {
img.setAttribute('x', newImgX);
img_mask.setAttribute('x', newImgX);
}
} else if ((newLabelX - imgWidth) > this.$bar.getX() && x < 0 && labelEndX > viewportCentral) {
label.setAttribute('x', newLabelX);
if (img) {
img.setAttribute('x', newImgX);
img_mask.setAttribute('x', newImgX);
}
}
}
date_changed() {
let changed = false;
const { new_start_date, new_end_date } = this.compute_start_end_date();
if (Number(this.task._start) !== Number(new_start_date)) {
changed = true;
this.task._start = new_start_date;
}
if (Number(this.task._end) !== Number(new_end_date)) {
changed = true;
this.task._end = new_end_date;
}
if (!changed) return;
this.gantt.trigger_event("date_change", [
this.task,
new_start_date,
date_utils.add(new_end_date, -1, "second"),
]);
}
progress_changed() {
const new_progress = this.compute_progress();
this.task.progress = new_progress;
this.gantt.trigger_event("progress_change", [this.task, new_progress]);
}
set_action_completed() {
this.action_completed = true;
setTimeout(() => (this.action_completed = false), 1000);
}
compute_start_end_date() {
const bar = this.$bar;
const x_in_units = bar.getX() / this.gantt.options.column_width;
const new_start_date = date_utils.add(
this.gantt.gantt_start,
x_in_units * this.gantt.options.step,
"hour",
);
const width_in_units = bar.getWidth() / this.gantt.options.column_width;
const new_end_date = date_utils.add(
new_start_date,
width_in_units * this.gantt.options.step,
"hour",
);
return { new_start_date, new_end_date };
}
compute_progress() {
const progress =
(this.$bar_progress.getWidth() / this.$bar.getWidth()) * 100;
return parseInt(progress, 10);
}
compute_expected_progress() {
this.expected_progress =
date_utils.diff(date_utils.today(), this.task._start, "hour") /
this.gantt.options.step;
this.expected_progress =
((this.expected_progress < this.duration
? this.expected_progress
: this.duration) *
100) /
this.duration;
}
compute_x() {
const { step, column_width } = this.gantt.options;
const task_start = this.task._start;
const gantt_start = this.gantt.gantt_start;
const diff = date_utils.diff(task_start, gantt_start, "hour");
let x = (diff / step) * column_width;
if (this.gantt.view_is("Month")) {
const diff = date_utils.diff(task_start, gantt_start, "day");
x = (diff * column_width) / 30;
}
this.x = x;
}
compute_y() {
this.y =
this.gantt.options.header_height +
this.gantt.options.padding +
this.task._index * (this.height + this.gantt.options.padding);
}
compute_duration() {
this.duration =
date_utils.diff(this.task._end, this.task._start, "hour") /
this.gantt.options.step;
}
get_snap_position(dx) {
let odx = dx,
rem,
position;
if (this.gantt.view_is("Week")) {
rem = dx % (this.gantt.options.column_width / 7);
position =
odx -
rem +
(rem < this.gantt.options.column_width / 14
? 0
: this.gantt.options.column_width / 7);
} else if (this.gantt.view_is("Month")) {
rem = dx % (this.gantt.options.column_width / 30);
position =
odx -
rem +
(rem < this.gantt.options.column_width / 60
? 0
: this.gantt.options.column_width / 30);
} else {
rem = dx % this.gantt.options.column_width;
position =
odx -
rem +
(rem < this.gantt.options.column_width / 2
? 0
: this.gantt.options.column_width);
}
return position;
}
update_attr(element, attr, value) {
value = +value;
if (!isNaN(value)) {
element.setAttribute(attr, value);
}
return element;
}
update_expected_progressbar_position() {
if (this.invalid) return;
this.$expected_bar_progress.setAttribute("x", this.$bar.getX());
this.compute_expected_progress();
this.$expected_bar_progress.setAttribute(
"width",
this.gantt.options.column_width *
this.duration *
(this.expected_progress / 100) || 0,
);
}
update_progressbar_position() {
if (this.invalid || this.gantt.options.readonly) return;
this.$bar_progress.setAttribute("x", this.$bar.getX());
this.$bar_progress.setAttribute(
"width",
this.$bar.getWidth() * (this.task.progress / 100),
);
}
update_label_position() {
const img_mask = this.bar_group.querySelector('.img_mask') || '';
const bar = this.$bar,
label = this.group.querySelector(".bar-label"),
img = this.group.querySelector('.bar-img');
let padding = 5;
let x_offset_label_img = this.image_size + 10;
const labelWidth = label.getBBox().width;
const barWidth = bar.getWidth();
if (labelWidth > barWidth) {
label.classList.add("big");
if (img) {
img.setAttribute('x', bar.getX() + bar.getWidth() + padding);
img_mask.setAttribute('x', bar.getX() + bar.getWidth() + padding);
label.setAttribute('x', bar.getX() + bar.getWidth() + x_offset_label_img);
} else {
label.setAttribute('x', bar.getX() + bar.getWidth() + padding);
}
} else {
label.classList.remove("big");
if (img) {
img.setAttribute('x', bar.getX() + padding);
img_mask.setAttribute('x', bar.getX() + padding);
label.setAttribute('x', bar.getX() + barWidth / 2 + x_offset_label_img);
} else {
label.setAttribute('x', bar.getX() + barWidth / 2 - labelWidth / 2);
}
}
}
update_handle_position() {
if (this.invalid || this.gantt.options.readonly) return;
const bar = this.$bar;
this.handle_group
.querySelector(".handle.left")
.setAttribute("x", bar.getX() - 12);
this.handle_group
.querySelector(".handle.right")
.setAttribute("x", bar.getEndX() + 4);
const handle = this.group.querySelector(".handle.progress");
handle && handle.setAttribute("points", this.get_progress_polygon_points());
}
update_arrow_position() {
this.arrows = this.arrows || [];
for (let arrow of this.arrows) {
arrow.update();
}
}
}
class Arrow {
constructor(gantt, from_task, to_task) {
this.gantt = gantt;
this.from_task = from_task;
this.to_task = to_task;
this.calculate_path();
this.draw();
}
calculate_path() {
let start_x =
this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2;
const condition = () =>
this.to_task.$bar.getX() < start_x + this.gantt.options.padding &&
start_x > this.from_task.$bar.getX() + this.gantt.options.padding;
while (condition()) {
start_x -= 10;
}
const start_y =
this.gantt.options.header_height +
this.gantt.options.bar_height +
(this.gantt.options.padding + this.gantt.options.bar_height) *
this.from_task.task._index +
this.gantt.options.padding;
const end_x = this.to_task.$bar.getX() - this.gantt.options.padding / 2 - 7;
const end_y =
this.gantt.options.header_height +
this.gantt.options.bar_height / 2 +
(this.gantt.options.padding + this.gantt.options.bar_height) *
this.to_task.task._index +
this.gantt.options.padding;
const from_is_below_to =
this.from_task.task._index > this.to_task.task._index;
const curve = this.gantt.options.arrow_curve;
const clockwise = from_is_below_to ? 1 : 0;
const curve_y = from_is_below_to ? -curve : curve;
const offset = from_is_below_to
? end_y + this.gantt.options.arrow_curve
: end_y - this.gantt.options.arrow_curve;
this.path = `
M ${start_x} ${start_y}
V ${offset}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;
if (
this.to_task.$bar.getX() <
this.from_task.$bar.getX() + this.gantt.options.padding
) {
const down_1 = this.gantt.options.padding / 2 - curve;
const down_2 =
this.to_task.$bar.getY() + this.to_task.$bar.getHeight() / 2 - curve_y;
const left = this.to_task.$bar.getX() - this.gantt.options.padding;
this.path = `
M ${start_x} ${start_y}
v ${down_1}
a ${curve} ${curve} 0 0 1 -${curve} ${curve}
H ${left}
a ${curve} ${curve} 0 0 ${clockwise} -${curve} ${curve_y}
V ${down_2}
a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y}
L ${end_x} ${end_y}
m -5 -5
l 5 5
l -5 5`;
}
}
draw() {
this.element = createSVG("path", {
d: this.path,
"data-from": this.from_task.task.id,
"data-to": this.to_task.task.id,
});
}
update() {
this.calculate_path();
this.element.setAttribute("d", this.path);
}
}
class Popup {
constructor(parent, custom_html) {
this.parent = parent;
this.custom_html = custom_html;
this.make();
}
make() {
this.parent.innerHTML = `