UNPKG

frappe-gantt

Version:

A simple, modern, interactive gantt library for the web

1,345 lines 62.3 kB
const v = "year", k = "month", M = "day", D = "hour", Y = "minute", L = "second", S = "millisecond", d = { parse_duration(n) { const e = /([0-9]+)(y|m|d|h|min|s|ms)/gm.exec(n); if (e !== null) { if (e[2] === "y") return { duration: parseInt(e[1]), scale: "year" }; if (e[2] === "m") return { duration: parseInt(e[1]), scale: "month" }; if (e[2] === "d") return { duration: parseInt(e[1]), scale: "day" }; if (e[2] === "h") return { duration: parseInt(e[1]), scale: "hour" }; if (e[2] === "min") return { duration: parseInt(e[1]), scale: "minute" }; if (e[2] === "s") return { duration: parseInt(e[1]), scale: "second" }; if (e[2] === "ms") return { duration: parseInt(e[1]), scale: "millisecond" }; } }, parse(n, t = "-", e = /[.:]/) { if (n instanceof Date) return n; if (typeof n == "string") { let i, s; const r = n.split(" "); i = r[0].split(t).map((o) => parseInt(o, 10)), s = r[1] && r[1].split(e), i[1] = i[1] ? i[1] - 1 : 0; let a = i; return s && s.length && (s.length === 4 && (s[3] = "0." + s[3], s[3] = parseFloat(s[3]) * 1e3), a = a.concat(s)), new Date(...a); } }, to_string(n, t = !1) { if (!(n instanceof Date)) throw new TypeError("Invalid argument type"); const e = this.get_date_values(n).map((r, a) => (a === 1 && (r = r + 1), a === 6 ? x(r + "", 3, "0") : x(r + "", 2, "0"))), i = `${e[0]}-${e[1]}-${e[2]}`, s = `${e[3]}:${e[4]}:${e[5]}.${e[6]}`; return i + (t ? " " + s : ""); }, format(n, t = "YYYY-MM-DD HH:mm:ss.SSS", e = "en") { const i = new Intl.DateTimeFormat(e, { month: "long" }), s = new Intl.DateTimeFormat(e, { month: "short" }), r = i.format(n), a = r.charAt(0).toUpperCase() + r.slice(1), o = this.get_date_values(n).map((g) => x(g, 2, 0)), h = { YYYY: o[0], MM: x(+o[1] + 1, 2, 0), DD: o[2], HH: o[3], mm: o[4], ss: o[5], SSS: o[6], D: o[2], MMMM: a, MMM: s.format(n) }; let l = t; const _ = []; return Object.keys(h).sort((g, c) => c.length - g.length).forEach((g) => { l.includes(g) && (l = l.replaceAll(g, `$${_.length}`), _.push(h[g])); }), _.forEach((g, c) => { l = l.replaceAll(`$${c}`, g); }), l; }, diff(n, t, e = "day") { let i, s, r, a, o, h, l; i = n - t + (t.getTimezoneOffset() - n.getTimezoneOffset()) * 6e4, s = i / 1e3, a = s / 60, r = a / 60, o = r / 24; let _ = n.getFullYear() - t.getFullYear(), g = n.getMonth() - t.getMonth(); return g += o % 30 / 30, h = _ * 12 + g, n.getDate() < t.getDate() && h--, l = h / 12, e.endsWith("s") || (e += "s"), Math.round( { milliseconds: i, seconds: s, minutes: a, hours: r, days: o, months: h, years: l }[e] * 100 ) / 100; }, today() { const n = this.get_date_values(/* @__PURE__ */ new Date()).slice(0, 3); return new Date(...n); }, now() { return /* @__PURE__ */ new Date(); }, add(n, t, e) { t = parseInt(t, 10); const i = [ n.getFullYear() + (e === v ? t : 0), n.getMonth() + (e === k ? t : 0), n.getDate() + (e === M ? t : 0), n.getHours() + (e === D ? t : 0), n.getMinutes() + (e === Y ? t : 0), n.getSeconds() + (e === L ? t : 0), n.getMilliseconds() + (e === S ? t : 0) ]; return new Date(...i); }, start_of(n, t) { const e = { [v]: 6, [k]: 5, [M]: 4, [D]: 3, [Y]: 2, [L]: 1, [S]: 0 }; function i(r) { const a = e[t]; return e[r] <= a; } const s = [ n.getFullYear(), i(v) ? 0 : n.getMonth(), i(k) ? 1 : n.getDate(), i(M) ? 0 : n.getHours(), i(D) ? 0 : n.getMinutes(), i(Y) ? 0 : n.getSeconds(), i(L) ? 0 : n.getMilliseconds() ]; return new Date(...s); }, clone(n) { return new Date(...this.get_date_values(n)); }, get_date_values(n) { return [ n.getFullYear(), n.getMonth(), n.getDate(), n.getHours(), n.getMinutes(), n.getSeconds(), n.getMilliseconds() ]; }, convert_scales(n, t) { const e = { millisecond: 11574074074074074e-24, second: 11574074074074073e-21, minute: 6944444444444445e-19, hour: 0.041666666666666664, day: 1, month: 30, year: 365 }, { duration: i, scale: s } = this.parse_duration(n); return i * e[s] / e[t]; }, get_days_in_month(n) { const t = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], e = n.getMonth(); if (e !== 1) return t[e]; const i = n.getFullYear(); return i % 4 === 0 && i % 100 != 0 || i % 400 === 0 ? 29 : 28; }, get_days_in_year(n) { return n.getFullYear() % 4 ? 365 : 366; } }; function x(n, t, e) { return n = n + "", t = t >> 0, e = String(typeof e < "u" ? e : " "), n.length > t ? String(n) : (t = t - n.length, t > e.length && (e += e.repeat(t / e.length)), e.slice(0, t) + String(n)); } function p(n, t) { return typeof n == "string" ? (t || document).querySelector(n) : n || null; } function f(n, t) { const e = document.createElementNS("http://www.w3.org/2000/svg", n); for (let i in t) i === "append_to" ? t.append_to.appendChild(e) : i === "innerHTML" ? e.innerHTML = t.innerHTML : i === "clipPath" ? e.setAttribute("clip-path", "url(#" + t[i] + ")") : e.setAttribute(i, t[i]); return e; } function T(n, t, e, i) { const s = W(n, t, e, i); if (s === n) { const r = document.createEvent("HTMLEvents"); r.initEvent("click", !0, !0), r.eventName = "click", s.dispatchEvent(r); } } function W(n, t, e, i, s = "0.4s", r = "0.1s") { const a = n.querySelector("animate"); if (a) return p.attr(a, { attributeName: t, from: e, to: i, dur: s, begin: "click + " + r // artificial click }), n; const o = f("animate", { attributeName: t, from: e, to: i, dur: s, begin: r, calcMode: "spline", values: e + ";" + i, keyTimes: "0; 1", keySplines: q("ease-out") }); return n.appendChild(o), n; } function q(n) { 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" }[n]; } p.on = (n, t, e, i) => { i ? p.delegate(n, t, e, i) : (i = e, p.bind(n, t, i)); }; p.off = (n, t, e) => { n.removeEventListener(t, e); }; p.bind = (n, t, e) => { t.split(/\s+/).forEach(function(i) { n.addEventListener(i, e); }); }; p.delegate = (n, t, e, i) => { n.addEventListener(t, function(s) { const r = s.target.closest(e); r && (s.delegatedTarget = r, i.call(this, s, r)); }); }; p.closest = (n, t) => t ? t.matches(n) ? t : p.closest(n, t.parentNode) : null; p.attr = (n, t, e) => { if (!e && typeof t == "string") return n.getAttribute(t); if (typeof t == "object") { for (let i in t) p.attr(n, i, t[i]); return; } n.setAttribute(t, e); }; class C { constructor(t, e, i) { this.gantt = t, this.from_task = e, this.to_task = i, this.calculate_path(), this.draw(); } calculate_path() { let t = this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2; const e = () => this.to_task.$bar.getX() < t + this.gantt.options.padding && t > this.from_task.$bar.getX() + this.gantt.options.padding; for (; e(); ) t -= 10; t -= 10; let i = this.gantt.config.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 / 2, s = this.to_task.$bar.getX() - 13, r = this.gantt.config.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 / 2; const a = this.from_task.task._index > this.to_task.task._index; let o = this.gantt.options.arrow_curve; const h = a ? 1 : 0; let l = a ? -o : o; if (this.to_task.$bar.getX() <= this.from_task.$bar.getX() + this.gantt.options.padding) { let _ = this.gantt.options.padding / 2 - o; _ < 0 && (_ = 0, o = this.gantt.options.padding / 2, l = a ? -o : o); const g = this.to_task.$bar.getY() + this.to_task.$bar.getHeight() / 2 - l, c = this.to_task.$bar.getX() - this.gantt.options.padding; this.path = ` M ${t} ${i} v ${_} a ${o} ${o} 0 0 1 ${-o} ${o} H ${c} a ${o} ${o} 0 0 ${h} ${-o} ${l} V ${g} a ${o} ${o} 0 0 ${h} ${o} ${l} L ${s} ${r} m -5 -5 l 5 5 l -5 5`; } else { s < t + o && (o = s - t); let _ = a ? r + o : r - o; this.path = ` M ${t} ${i} V ${_} a ${o} ${o} 0 0 ${h} ${o} ${o} L ${s} ${r} m -5 -5 l 5 5 l -5 5`; } } draw() { this.element = f("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 F { constructor(t, e) { this.set_defaults(t, e), this.prepare_wrappers(), this.prepare_helpers(), this.refresh(); } refresh() { this.bar_group.innerHTML = "", this.handle_group.innerHTML = "", this.task.custom_class ? this.group.classList.add(this.task.custom_class) : this.group.classList = ["bar-wrapper"], this.prepare_values(), this.draw(), this.bind(); } set_defaults(t, e) { this.action_completed = !1, this.gantt = t, this.task = e, this.name = this.name || ""; } prepare_wrappers() { this.group = f("g", { class: "bar-wrapper" + (this.task.custom_class ? " " + this.task.custom_class : ""), "data-id": this.task.id }), this.bar_group = f("g", { class: "bar-group", append_to: this.group }), this.handle_group = f("g", { class: "handle-group", append_to: this.group }); } prepare_values() { this.invalid = this.task.invalid, this.height = this.gantt.options.bar_height, this.image_size = this.height - 5, this.task._start = new Date(this.task.start), this.task._end = new Date(this.task.end), this.compute_x(), this.compute_y(), this.compute_duration(), this.corner_radius = this.gantt.options.bar_corner_radius, this.width = this.gantt.config.column_width * this.duration, (!this.task.progress || this.task.progress < 0) && (this.task.progress = 0), this.task.progress > 100 && (this.task.progress = 100); } 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(), this.gantt.options.show_expected_progress && (this.prepare_expected_progress_values(), this.draw_expected_progress_bar()), this.draw_label(), this.draw_resize_handles(), this.task.thumbnail && this.draw_thumbnail(); } draw_bar() { this.$bar = f("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 }), this.task.color && (this.$bar.style.fill = this.task.color), T(this.$bar, "width", 0, this.width), this.invalid && this.$bar.classList.add("bar-invalid"); } draw_expected_progress_bar() { this.invalid || (this.$expected_bar_progress = f("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 }), T( this.$expected_bar_progress, "width", 0, this.expected_progress_width )); } draw_progress_bar() { if (this.invalid) return; this.progress_width = this.calculate_progress_width(); let t = this.corner_radius; /^((?!chrome|android).)*safari/i.test(navigator.userAgent) || (t = this.corner_radius + 2), this.$bar_progress = f("rect", { x: this.x, y: this.y, width: this.progress_width, height: this.height, rx: t, ry: t, class: "bar-progress", append_to: this.bar_group }), this.task.color_progress && (this.$bar_progress.style.fill = this.task.color); const e = d.diff( this.task._start, this.gantt.gantt_start, this.gantt.config.unit ) / this.gantt.config.step * this.gantt.config.column_width; let i = this.gantt.create_el({ classes: `date-range-highlight hide highlight-${this.task.id}`, width: this.width, left: e }); this.$date_highlight = i, this.gantt.$lower_header.prepend(this.$date_highlight), T(this.$bar_progress, "width", 0, this.progress_width); } calculate_progress_width() { const t = this.$bar.getWidth(), e = this.x + t, i = this.gantt.config.ignored_positions.reduce((h, l) => h + (l >= this.x && l < e), 0) * this.gantt.config.column_width; let s = (t - i) * this.task.progress / 100; const r = this.x + s, a = this.gantt.config.ignored_positions.reduce((h, l) => h + (l >= this.x && l < r), 0) * this.gantt.config.column_width; s += a; let o = this.gantt.get_ignored_region( this.x + s ); for (; o.length; ) s += this.gantt.config.column_width, o = this.gantt.get_ignored_region( this.x + s ); return this.progress_width = s, s; } draw_label() { let t = this.x + this.$bar.getWidth() / 2; this.task.thumbnail && (t = this.x + this.image_size + 5), f("text", { x: t, y: this.y + this.height / 2, innerHTML: this.task.name, class: "bar-label", append_to: this.bar_group }), requestAnimationFrame(() => this.update_label_position()); } draw_thumbnail() { let t = 10, e = 2, i, s; i = f("defs", { append_to: this.bar_group }), f("rect", { id: "rect_" + this.task.id, x: this.x + t, y: this.y + e, width: this.image_size, height: this.image_size, rx: "15", class: "img_mask", append_to: i }), s = f("clipPath", { id: "clip_" + this.task.id, append_to: i }), f("use", { href: "#rect_" + this.task.id, append_to: s }), f("image", { x: this.x + t, y: this.y + e, 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 t = this.$bar, e = 3; if (this.handles = [], this.gantt.options.readonly_dates || (this.handles.push( f("rect", { x: t.getEndX() - e / 2, y: t.getY() + this.height / 4, width: e, height: this.height / 2, rx: 2, ry: 2, class: "handle right", append_to: this.handle_group }) ), this.handles.push( f("rect", { x: t.getX() - e / 2, y: t.getY() + this.height / 4, width: e, height: this.height / 2, rx: 2, ry: 2, class: "handle left", append_to: this.handle_group }) )), !this.gantt.options.readonly_progress) { const i = this.$bar_progress; this.$handle_progress = f("circle", { cx: i.getEndX(), cy: i.getY() + i.getHeight() / 2, r: 4.5, class: "handle progress", append_to: this.handle_group }), this.handles.push(this.$handle_progress); } for (let i of this.handles) p.on(i, "mouseenter", () => i.classList.add("active")), p.on(i, "mouseleave", () => i.classList.remove("active")); } bind() { this.invalid || this.setup_click_event(); } setup_click_event() { let t = this.task.id; p.on(this.group, "mouseover", (i) => { this.gantt.trigger_event("hover", [ this.task, i.screenX, i.screenY, i ]); }), this.gantt.options.popup_on === "click" && p.on(this.group, "mouseup", (i) => { const s = i.offsetX || i.layerX; if (this.$handle_progress) { const r = +this.$handle_progress.getAttribute("cx"); if (r > s - 1 && r < s + 1 || this.gantt.bar_being_dragged) return; } this.gantt.show_popup({ x: i.offsetX || i.layerX, y: i.offsetY || i.layerY, task: this.task, target: this.$bar }); }); let e; p.on(this.group, "mouseenter", (i) => { e = setTimeout(() => { this.gantt.options.popup_on === "hover" && this.gantt.show_popup({ x: i.offsetX || i.layerX, y: i.offsetY || i.layerY, task: this.task, target: this.$bar }), this.gantt.$container.querySelector(`.highlight-${t}`).classList.remove("hide"); }, 200); }), p.on(this.group, "mouseleave", () => { var i, s; clearTimeout(e), this.gantt.options.popup_on === "hover" && ((s = (i = this.gantt.popup) == null ? void 0 : i.hide) == null || s.call(i)), this.gantt.$container.querySelector(`.highlight-${t}`).classList.add("hide"); }), p.on(this.group, "click", () => { this.gantt.trigger_event("click", [this.task]); }), p.on(this.group, "dblclick", (i) => { this.action_completed || (this.group.classList.remove("active"), this.gantt.popup && this.gantt.popup.parent.classList.remove("hide"), this.gantt.trigger_event("double_click", [this.task])); }); } update_bar_position({ x: t = null, width: e = null }) { const i = this.$bar; if (t) { if (!this.task.dependencies.map((a) => this.gantt.get_bar(a).$bar.getX()).reduce((a, o) => t >= o, t)) return; this.update_attr(i, "x", t), this.x = t, this.$date_highlight.style.left = t + "px"; } e > 0 && (this.update_attr(i, "width", e), this.$date_highlight.style.width = e + "px"), this.update_label_position(), this.update_handle_position(), this.date_changed(), this.compute_duration(), this.gantt.options.show_expected_progress && this.update_expected_progressbar_position(), this.update_progressbar_position(), this.update_arrow_position(); } update_label_position_on_horizontal_scroll({ x: t, sx: e }) { const i = this.gantt.$container.querySelector(".gantt-container"), s = this.group.querySelector(".bar-label"), r = this.group.querySelector(".bar-img") || "", a = this.bar_group.querySelector(".img_mask") || ""; let o = this.$bar.getX() + this.$bar.getWidth(), h = s.getX() + t, l = r && r.getX() + t || 0, _ = r && r.getBBox().width + 7 || 7, g = h + s.getBBox().width + 7, c = e + i.clientWidth / 2; s.classList.contains("big") || (g < o && t > 0 && g < c || h - _ > this.$bar.getX() && t < 0 && g > c) && (s.setAttribute("x", h), r && (r.setAttribute("x", l), a.setAttribute("x", l))); } date_changed() { let t = !1; const { new_start_date: e, new_end_date: i } = this.compute_start_end_date(); Number(this.task._start) !== Number(e) && (t = !0, this.task._start = e), Number(this.task._end) !== Number(i) && (t = !0, this.task._end = i), t && this.gantt.trigger_event("date_change", [ this.task, e, d.add(i, -1, "second") ]); } progress_changed() { this.task.progress = this.compute_progress(), this.gantt.trigger_event("progress_change", [ this.task, this.task.progress ]); } set_action_completed() { this.action_completed = !0, setTimeout(() => this.action_completed = !1, 1e3); } compute_start_end_date() { const t = this.$bar, e = t.getX() / this.gantt.config.column_width; let i = d.add( this.gantt.gantt_start, e * this.gantt.config.step, this.gantt.config.unit ); const s = t.getWidth() / this.gantt.config.column_width, r = d.add( i, s * this.gantt.config.step, this.gantt.config.unit ); return { new_start_date: i, new_end_date: r }; } compute_progress() { this.progress_width = this.$bar_progress.getWidth(), this.x = this.$bar_progress.getBBox().x; const t = this.x + this.progress_width, e = this.progress_width - this.gantt.config.ignored_positions.reduce((s, r) => s + (r >= this.x && r <= t), 0) * this.gantt.config.column_width; if (e < 0) return 0; const i = this.$bar.getWidth() - this.ignored_duration_raw * this.gantt.config.column_width; return parseInt(e / i * 100, 10); } compute_expected_progress() { this.expected_progress = d.diff(d.today(), this.task._start, "hour") / this.gantt.config.step, this.expected_progress = (this.expected_progress < this.duration ? this.expected_progress : this.duration) * 100 / this.duration; } compute_x() { const { column_width: t } = this.gantt.config, e = this.task._start, i = this.gantt.gantt_start; let r = d.diff(e, i, this.gantt.config.unit) / this.gantt.config.step * t; this.x = r; } compute_y() { this.y = this.gantt.config.header_height + this.gantt.options.padding / 2 + this.task._index * (this.height + this.gantt.options.padding); } compute_duration() { let t = 0, e = 0; for (let i = new Date(this.task._start); i < this.task._end; i.setDate(i.getDate() + 1)) e++, !this.gantt.config.ignored_dates.find( (s) => s.getTime() === i.getTime() ) && (!this.gantt.config.ignored_function || !this.gantt.config.ignored_function(i)) && t++; this.task.actual_duration = t, this.task.ignored_duration = e - t, this.duration = d.convert_scales( e + "d", this.gantt.config.unit ) / this.gantt.config.step, this.actual_duration_raw = d.convert_scales( t + "d", this.gantt.config.unit ) / this.gantt.config.step, this.ignored_duration_raw = this.duration - this.actual_duration_raw; } update_attr(t, e, i) { return i = +i, isNaN(i) || t.setAttribute(e, i), t; } update_expected_progressbar_position() { this.invalid || (this.$expected_bar_progress.setAttribute("x", this.$bar.getX()), this.compute_expected_progress(), this.$expected_bar_progress.setAttribute( "width", this.gantt.config.column_width * this.actual_duration_raw * (this.expected_progress / 100) || 0 )); } update_progressbar_position() { this.invalid || this.gantt.options.readonly || (this.$bar_progress.setAttribute("x", this.$bar.getX()), this.$bar_progress.setAttribute( "width", this.calculate_progress_width() )); } update_label_position() { const t = this.bar_group.querySelector(".img_mask") || "", e = this.$bar, i = this.group.querySelector(".bar-label"), s = this.group.querySelector(".bar-img"); let r = 5, a = this.image_size + 10; const o = i.getBBox().width, h = e.getWidth(); o > h ? (i.classList.add("big"), s ? (s.setAttribute("x", e.getEndX() + r), t.setAttribute("x", e.getEndX() + r), i.setAttribute("x", e.getEndX() + a)) : i.setAttribute("x", e.getEndX() + r)) : (i.classList.remove("big"), s ? (s.setAttribute("x", e.getX() + r), t.setAttribute("x", e.getX() + r), i.setAttribute( "x", e.getX() + h / 2 + a )) : i.setAttribute( "x", e.getX() + h / 2 - o / 2 )); } update_handle_position() { if (this.invalid || this.gantt.options.readonly) return; const t = this.$bar; this.handle_group.querySelector(".handle.left").setAttribute("x", t.getX()), this.handle_group.querySelector(".handle.right").setAttribute("x", t.getEndX()); const e = this.group.querySelector(".handle.progress"); e && e.setAttribute("cx", this.$bar_progress.getEndX()); } update_arrow_position() { this.arrows = this.arrows || []; for (let t of this.arrows) t.update(); } } class O { constructor(t, e, i) { this.parent = t, this.popup_func = e, this.gantt = i, this.make(); } make() { this.parent.innerHTML = ` <div class="title"></div> <div class="subtitle"></div> <div class="details"></div> <div class="actions"></div> `, this.hide(), this.title = this.parent.querySelector(".title"), this.subtitle = this.parent.querySelector(".subtitle"), this.details = this.parent.querySelector(".details"), this.actions = this.parent.querySelector(".actions"); } show({ x: t, y: e, task: i, target: s }) { this.actions.innerHTML = ""; let r = this.popup_func({ task: i, chart: this.gantt, get_title: () => this.title, set_title: (a) => this.title.innerHTML = a, get_subtitle: () => this.subtitle, set_subtitle: (a) => this.subtitle.innerHTML = a, get_details: () => this.details, set_details: (a) => this.details.innerHTML = a, add_action: (a, o) => { let h = this.gantt.create_el({ classes: "action-btn", type: "button", append_to: this.actions }); typeof a == "function" && (a = a(i)), h.innerHTML = a, h.onclick = (l) => o(i, this.gantt, l); } }); r !== !1 && (r && (this.parent.innerHTML = r), this.actions.innerHTML === "" ? this.actions.remove() : this.parent.appendChild(this.actions), this.parent.style.left = t + 10 + "px", this.parent.style.top = e - 10 + "px", this.parent.classList.remove("hide")); } hide() { this.parent.classList.add("hide"); } } function A(n) { const t = n.getFullYear(); return t - t % 10 + ""; } function I(n, t, e) { let i = d.add(n, 6, "day"), s = i.getMonth() !== n.getMonth() ? "D MMM" : "D", r = !t || n.getMonth() !== t.getMonth() ? "D MMM" : "D"; return `${d.format(n, r, e)} - ${d.format(i, s, e)}`; } const b = [ { name: "Hour", padding: "7d", step: "1h", date_format: "YYYY-MM-DD HH:", lower_text: "HH", upper_text: (n, t, e) => !t || n.getDate() !== t.getDate() ? d.format(n, "D MMMM", e) : "", upper_text_frequency: 24 }, { name: "Quarter Day", padding: "7d", step: "6h", date_format: "YYYY-MM-DD HH:", lower_text: "HH", upper_text: (n, t, e) => !t || n.getDate() !== t.getDate() ? d.format(n, "D MMM", e) : "", upper_text_frequency: 4 }, { name: "Half Day", padding: "14d", step: "12h", date_format: "YYYY-MM-DD HH:", lower_text: "HH", upper_text: (n, t, e) => !t || n.getDate() !== t.getDate() ? n.getMonth() !== n.getMonth() ? d.format(n, "D MMM", e) : d.format(n, "D", e) : "", upper_text_frequency: 2 }, { name: "Day", padding: "7d", date_format: "YYYY-MM-DD", step: "1d", lower_text: (n, t, e) => !t || n.getDate() !== t.getDate() ? d.format(n, "D", e) : "", upper_text: (n, t, e) => !t || n.getMonth() !== t.getMonth() ? d.format(n, "MMMM", e) : "", thick_line: (n) => n.getDay() === 1 }, { name: "Week", padding: "1m", step: "7d", date_format: "YYYY-MM-DD", column_width: 140, lower_text: I, upper_text: (n, t, e) => !t || n.getMonth() !== t.getMonth() ? d.format(n, "MMMM", e) : "", thick_line: (n) => n.getDate() >= 1 && n.getDate() <= 7, upper_text_frequency: 4 }, { name: "Month", padding: "2m", step: "1m", column_width: 120, date_format: "YYYY-MM", lower_text: "MMMM", upper_text: (n, t, e) => !t || n.getFullYear() !== t.getFullYear() ? d.format(n, "YYYY", e) : "", thick_line: (n) => n.getMonth() % 3 === 0, snap_at: "7d" }, { name: "Year", padding: "2y", step: "1y", column_width: 120, date_format: "YYYY", upper_text: (n, t, e) => !t || A(n) !== A(t) ? A(n) : "", lower_text: "YYYY", snap_at: "30d" } ], z = { arrow_curve: 5, auto_move_label: !1, bar_corner_radius: 3, bar_height: 30, container_height: "auto", column_width: null, date_format: "YYYY-MM-DD HH:mm", upper_header_height: 45, lower_header_height: 30, snap_at: null, infinite_padding: !0, holidays: { "var(--g-weekend-highlight-color)": "weekend" }, ignore: [], language: "en", lines: "both", move_dependencies: !0, padding: 18, popup: (n) => { n.set_title(n.task.name), n.task.description ? n.set_subtitle(n.task.description) : n.set_subtitle(""); const t = d.format( n.task._start, "MMM D", n.chart.options.language ), e = d.format( d.add(n.task._end, -1, "second"), "MMM D", n.chart.options.language ); n.set_details( `${t} - ${e} (${n.task.actual_duration} days${n.task.ignored_duration ? " + " + n.task.ignored_duration + " excluded" : ""})<br/>Progress: ${Math.floor(n.task.progress * 100) / 100}%` ); }, popup_on: "click", readonly_progress: !1, readonly_dates: !1, readonly: !1, scroll_to: "today", show_expected_progress: !1, today_button: !0, view_mode: "Day", view_mode_select: !1, view_modes: b }; class B { constructor(t, e, i) { this.setup_wrapper(t), this.setup_options(i), this.setup_tasks(e), this.change_view_mode(), this.bind_events(); } setup_wrapper(t) { let e, i; if (typeof t == "string") { let s = document.querySelector(t); if (!s) throw new ReferenceError( `CSS selector "${t}" could not be found in DOM` ); t = s; } if (t instanceof HTMLElement) i = t, e = t.querySelector("svg"); else if (t instanceof SVGElement) e = t; else throw new TypeError( "Frappe Gantt only supports usage of a string CSS selector, HTML DOM element or SVG DOM element for the 'element' parameter" ); e ? (this.$svg = e, this.$svg.classList.add("gantt")) : this.$svg = f("svg", { append_to: i, class: "gantt" }), this.$container = this.create_el({ classes: "gantt-container", append_to: this.$svg.parentElement }), this.$container.appendChild(this.$svg), this.$popup_wrapper = this.create_el({ classes: "popup-wrapper", append_to: this.$container }); } setup_options(t) { this.original_options = t, this.options = { ...z, ...t }; const e = { "grid-height": "container_height", "bar-height": "bar_height", "lower-header-height": "lower_header_height", "upper-header-height": "upper_header_height" }; for (let i in e) { let s = this.options[e[i]]; s !== "auto" && this.$container.style.setProperty( "--gv-" + i, s + "px" ); } if (this.config = { ignored_dates: [], ignored_positions: [], extend_by_units: 10 }, typeof this.options.ignore != "function") { typeof this.options.ignore == "string" && (this.options.ignore = [this.options.ignord]); for (let i of this.options.ignore) { if (typeof i == "function") { this.config.ignored_function = i; continue; } typeof i == "string" && (i === "weekend" ? this.config.ignored_function = (s) => s.getDay() == 6 || s.getDay() == 0 : this.config.ignored_dates.push(/* @__PURE__ */ new Date(i + " "))); } } else this.config.ignored_function = this.options.ignore; } update_options(t) { this.setup_options({ ...this.original_options, ...t }), this.change_view_mode(void 0, !0); } setup_tasks(t) { this.tasks = t.map((e, i) => { if (!e.start) return console.error( `task "${e.id}" doesn't have a start date` ), !1; if (e._start = d.parse(e.start), e.end === void 0 && e.duration !== void 0 && (e.end = e._start, e.duration.split(" ").forEach((o) => { let { duration: h, scale: l } = d.parse_duration(o); e.end = d.add(e.end, h, l); })), !e.end) return console.error(`task "${e.id}" doesn't have an end date`), !1; if (e._end = d.parse(e.end), d.diff(e._end, e._start, "year") < 0) return console.error( `start of task can't be after end of task: in task "${e.id}"` ), !1; if (d.diff(e._end, e._start, "year") > 10) return console.error( `the duration of task "${e.id}" is too long (above ten years)` ), !1; if (e._index = i, d.get_date_values(e._end).slice(3).every((a) => a === 0) && (e._end = d.add(e._end, 24, "hour")), typeof e.dependencies == "string" || !e.dependencies) { let a = []; e.dependencies && (a = e.dependencies.split(",").map((o) => o.trim().replaceAll(" ", "_")).filter((o) => o)), e.dependencies = a; } return e.id ? typeof e.id == "string" ? e.id = e.id.replaceAll(" ", "_") : e.id = `${e.id}` : e.id = N(e), e; }).filter((e) => e), this.setup_dependencies(); } setup_dependencies() { this.dependency_map = {}; for (let t of this.tasks) for (let e of t.dependencies) this.dependency_map[e] = this.dependency_map[e] || [], this.dependency_map[e].push(t.id); } refresh(t) { this.setup_tasks(t), this.change_view_mode(); } update_task(t, e) { let i = this.tasks.find((r) => r.id === t), s = this.bars[i._index]; Object.assign(i, e), s.refresh(); } change_view_mode(t = this.options.view_mode, e = !1) { typeof t == "string" && (t = this.options.view_modes.find((r) => r.name === t)); let i, s; e && (i = this.$container.scrollLeft, s = this.options.scroll_to, this.options.scroll_to = null), this.options.view_mode = t.name, this.config.view_mode = t, this.update_view_scale(t), this.setup_dates(e), this.render(), e && (this.$container.scrollLeft = i, this.options.scroll_to = s), this.trigger_event("view_change", [t]); } update_view_scale(t) { let { duration: e, scale: i } = d.parse_duration(t.step); this.config.step = e, this.config.unit = i, this.config.column_width = this.options.column_width || t.column_width || 45, this.$container.style.setProperty( "--gv-column-width", this.config.column_width + "px" ), this.config.header_height = this.options.lower_header_height + this.options.upper_header_height + 10; } setup_dates(t = !1) { this.setup_gantt_dates(t), this.setup_date_values(); } setup_gantt_dates(t) { let e, i; this.tasks.length || (e = /* @__PURE__ */ new Date(), i = /* @__PURE__ */ new Date()); for (let s of this.tasks) (!e || s._start < e) && (e = s._start), (!i || s._end > i) && (i = s._end); if (e = d.start_of(e, this.config.unit), i = d.start_of(i, this.config.unit), !t) if (this.options.infinite_padding) this.gantt_start = d.add( e, -this.config.extend_by_units * 3, this.config.unit ), this.gantt_end = d.add( i, this.config.extend_by_units * 3, this.config.unit ); else { typeof this.config.view_mode.padding == "string" && (this.config.view_mode.padding = [ this.config.view_mode.padding, this.config.view_mode.padding ]); let [s, r] = this.config.view_mode.padding.map( d.parse_duration ); this.gantt_start = d.add( e, -s.duration, s.scale ), this.gantt_end = d.add( i, r.duration, r.scale ); } this.config.date_format = this.config.view_mode.date_format || this.options.date_format, this.gantt_start.setHours(0, 0, 0, 0); } setup_date_values() { let t = this.gantt_start; for (this.dates = [t]; t < this.gantt_end; ) t = d.add( t, this.config.step, this.config.unit ), this.dates.push(t); } bind_events() { this.bind_grid_click(), this.bind_holiday_labels(), this.bind_bar_events(); } render() { this.clear(), this.setup_layers(), this.make_grid(), this.make_dates(), this.make_grid_extras(), this.make_bars(), this.make_arrows(), this.map_arrows_on_bars(), this.set_dimensions(), this.set_scroll_position(this.options.scroll_to); } setup_layers() { this.layers = {}; const t = ["grid", "arrow", "progress", "bar"]; for (let e of t) this.layers[e] = f("g", { class: e, append_to: this.$svg }); this.$extras = this.create_el({ classes: "extras", append_to: this.$container }), this.$adjust = this.create_el({ classes: "adjust hide", append_to: this.$extras, type: "button" }), this.$adjust.innerHTML = "&larr;"; } make_grid() { this.make_grid_background(), this.make_grid_rows(), this.make_grid_header(), this.make_side_header(); } make_grid_extras() { this.make_grid_highlights(), this.make_grid_ticks(); } make_grid_background() { const t = this.dates.length * this.config.column_width, e = Math.max( this.config.header_height + this.options.padding + (this.options.bar_height + this.options.padding) * this.tasks.length - 10, this.options.container_height !== "auto" ? this.options.container_height : 0 ); f("rect", { x: 0, y: 0, width: t, height: e, class: "grid-background", append_to: this.$svg }), p.attr(this.$svg, { height: e, width: "100%" }), this.grid_height = e, this.options.container_height === "auto" && (this.$container.style.height = e + "px"); } make_grid_rows() { const t = f("g", { append_to: this.layers.grid }), e = this.dates.length * this.config.column_width, i = this.options.bar_height + this.options.padding; this.config.header_height; for (let s = this.config.header_height; s < this.grid_height; s += i) f("rect", { x: 0, y: s, width: e, height: i, class: "grid-row", append_to: t }); } make_grid_header() { this.$header = this.create_el({ width: this.dates.length * this.config.column_width, classes: "grid-header", append_to: this.$container }), this.$upper_header = this.create_el({ classes: "upper-header", append_to: this.$header }), this.$lower_header = this.create_el({ classes: "lower-header", append_to: this.$header }); } make_side_header() { if (this.$side_header = this.create_el({ classes: "side-header" }), this.$upper_header.prepend(this.$side_header), this.options.view_mode_select) { const t = document.createElement("select"); t.classList.add("viewmode-select"); const e = document.createElement("option"); e.selected = !0, e.disabled = !0, e.textContent = "Mode", t.appendChild(e); for (const i of this.options.view_modes) { const s = document.createElement("option"); s.value = i.name, s.textContent = i.name, i.name === this.config.view_mode.name && (s.selected = !0), t.appendChild(s); } t.addEventListener( "change", (function() { this.change_view_mode(t.value, !0); }).bind(this) ), this.$side_header.appendChild(t); } if (this.options.today_button) { let t = document.createElement("button"); t.classList.add("today-button"), t.textContent = "Today", t.onclick = this.scroll_current.bind(this), this.$side_header.prepend(t), this.$today_button = t; } } make_grid_ticks() { if (this.options.lines === "none") return; let t = 0, e = this.config.header_height, i = this.grid_height - this.config.header_height, s = f("g", { class: "lines_layer", append_to: this.layers.grid }), r = this.config.header_height; const a = this.dates.length * this.config.column_width, o = this.options.bar_height + this.options.padding; if (this.options.lines !== "vertical") for (let h = this.config.header_height; h < this.grid_height; h += o) f("line", { x1: 0, y1: r + o, x2: a, y2: r + o, class: "row-line", append_to: s }), r += o; if (this.options.lines !== "horizontal") for (let h of this.dates) { let l = "tick"; this.config.view_mode.thick_line && this.config.view_mode.thick_line(h) && (l += " thick"), f("path", { d: `M ${t} ${e} v ${i}`, class: l, append_to: this.layers.grid }), this.view_is("month") ? t += d.get_days_in_month(h) * this.config.column_width / 30 : this.view_is("year") ? t += d.get_days_in_year(h) * this.config.column_width / 365 : t += this.config.column_width; } } highlight_holidays() { let t = {}; if (this.options.holidays) for (let e in this.options.holidays) { let i = this.options.holidays[e]; i === "weekend" && (i = (r) => r.getDay() === 0 || r.getDay() === 6); let s; if (typeof i == "object") { let r = i.find((a) => typeof a == "function"); if (r && (s = r), this.options.holidays.name) { let a = /* @__PURE__ */ new Date(i.date + " "); i = (o) => a.getTime() === o.getTime(), t[a] = i.name; } else i = (a) => this.options.holidays[e].filter((o) => typeof o != "function").map((o) => { if (o.name) { let h = /* @__PURE__ */ new Date(o.date + " "); return t[h] = o.name, h.getTime(); } return (/* @__PURE__ */ new Date(o + " ")).getTime(); }).includes(a.getTime()); } for (let r = new Date(this.gantt_start); r <= this.gantt_end; r.setDate(r.getDate() + 1)) if (!(this.config.ignored_dates.find( (a) => a.getTime() == r.getTime() ) || this.config.ignored_function && this.config.ignored_function(r)) && (i(r) || s && s(r))) { const a = d.diff( r, this.gantt_start, this.config.unit ) / this.config.step * this.config.column_width, o = this.grid_height - this.config.header_height, h = d.format(r, "YYYY-MM-DD", this.options.language).replace(" ", "_"); if (t[r]) { let l = this.create_el({ classes: "holiday-label label_" + h, append_to: this.$extras }); l.textContent = t[r]; } f("rect", { x: Math.round(a), y: this.config.header_height, width: this.config.column_width / d.convert_scales( this.config.view_mode.step, "day" ), height: o, class: "holiday-highlight " + h, style: `fill: ${e};`, append_to: this.layers.grid }); } } } /** * Compute the horizontal x-axis distance and associated date for the current date and view. * * @returns Object containing the x-axis distance and date of the current date, or null if the current date is out of the gantt range. */ highlight_current() { const t = this.get_closest_date(); if (!t) return; const [e, i] = t; i.classList.add("current-date-highlight"); const r = d.diff( /* @__PURE__ */ new Date(), this.gantt_start, this.config.unit ) / this.config.step * this.config.column_width; this.$current_highlight = this.create_el({ top: this.config.header_height, left: r, height: this.grid_height - this.config.header_height, classes: "current-highlight", append_to: this.$container }), this.$current_ball_highlight = this.create_el({ top: this.config.header_height - 6, left: r - 2.5, width: 6, height: 6, classes: "current-ball-highlight", append_to: this.$header }); } make_grid_highlights() { this.highlight_holidays(), this.config.ignored_positions = []; const t = (this.options.bar_height + this.options.padding) * this.tasks.length; this.layers.grid.innerHTML += `<pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="4" height="4"> <path d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2" style="stroke:grey; stroke-width:0.3" /> </pattern>`; for (let i = new Date(this.gantt_start); i <= this.gantt_end; i.setDate(i.getDate() + 1)) { if (!this.config.ignored_dates.find( (r) => r.getTime() == i.getTime() ) && (!this.config.ignored_function || !this.config.ignored_function(i))) continue; let s = d.convert_scales( d.diff(i, this.gantt_start) + "d", this.config.unit ) / this.config.step; this.config.ignored_positions.push(s * this.config.column_width), f("rect", { x: s * this.config.column_width, y: this.config.header_height, width: this.config.column_width, height: t, class: "ignored-bar", style: "fill: url(#diagonalHatch);", append_to: this.$svg }); } this.highlight_current( this.config.view_mode ); } create_el({ left: t, top: e, width: i, height: s, id: r, classes: a, append_to: o, type: h }) { let l = document.createElement(h || "div"); for (let _ of a.split(" ")) l.classList.add(_); return l.style.top = e + "px", l.style.left = t + "px", r && (l.id = r), i && (l.style.width = i + "px"), s && (l.style.height = s + "px"), o && o.appendChild(l), l; } make_dates() { this.get_dates_to_draw().forEach((t, e) => { if (t.lower_text) { let i = this.create_el({ left: t.x, top: t.lower_y, classes: "lower-text date_" + $(t.formatted_date), append_to: this.$lower_header }); i.innerText = t.lower_text; } if (t.upper_text) { let i = this.create_el({ left: t.x, top: t.upper_y, classes: "upper-text", append_to: this.$upper_header }); i.innerText = t.upper_text; } }), this.upperTexts = Array.from( this.$container.querySelectorAll(".upper-text") ); } get_dates_to_draw() { let t = null; return this.dates.map((i, s) => { const r = this.get_date_info(i, t, s); return t = r, r; }); } get_date_info(t, e) { let i = e ? e.date : null; this.config.column_width; const s = e ? e.x + e.column_width : 0; let r = this.config.view_mode.upper_text, a = this.config.view_mode.lower_text; return r ? typeof r == "string" && (this.config.view_mode.upper_text = (o) => d.format(o, r, this.options.language)) : this.config.view_mode.upper_text = () => "", a ? typeof a == "string" && (this.config.view_mode.lower_text = (o) => d.format(o, a, this.options.language)) : this.config.view_mode.lower_text = () => "", { date: t, formatted_date: $( d.format( t, this.config.date_format, this.options.language ) ), column_width: this.config.column_width, x: s, upper_text: this.config.view_mode.upper_text( t, i, this.options.language ), lower_text: this.config.view_mode.lower_text( t, i, this.options.language ), upper_y: 17, lower_y: this.options.upper_header_height + 5 }; } make_bars() { this.bars = this.tasks.map((t) => { const e = new F(this, t); return this.layers.bar.appendChild(e.group), e; }); } make_arrows() { this.arrows = []; for (let t of this.tasks) { let e = []; e = t.dependencies.map((i) => { const s = this.get_task(i); if (!s) return; const r = new C( this, this.bars[s._index], // from_task this.bars[t._index] // to_task ); return this.layers.arrow.appendChild(r.element), r; }).filter(Boolean), this.arrows = this.arrows.concat(e); } } map_arrows_on_bars() { for (let t of this.bars) t.arrows = this.arrows.filter((e) => e.from_task.task.id === t.task.id || e.to_task.task.id === t.task.id); } set_dimensions() { const { width: t } = this.$svg.getBoundingClientRect(), e = this.$svg.querySelector(".grid .grid-row") ? this.$svg.querySelector(".grid .grid-row").getAttribute("width") : 0; t < e && this.$svg.setAttribute("width", e); } set_scroll_position(t) { if (this.options.infinite_padding && (!t || t === "start")) { let [a, ...o] = this.get_start_end_positions(); this.$container.scrollLeft = a; return; } if (!t || t === "start") t = this.gantt_start; else if (t === "end") t = this.gantt_end; else { if (t === "today") return this.scroll_current(); typeof t == "string" && (t = d.parse(t)); } const i = d.diff( t, this.gantt_start, this.config.unit ) / this.config.step * this.config.column_width; this.$container.scrollTo({ left: i - this.config.column_width / 6, behavior: "smooth" }), this.$current && this.$current.classList.remove("current-upper"), this.current_date = d.add( this.gantt_start, this.$container.scrollLeft / this.config.column_width, this.config.unit ); let s = this.config.view_mode.upper_text( this.current_date, null, this.options.language ), r = this.upperTexts.find( (a) => a.textContent === s ); this.current_date = d.add( this.gantt_start, (this.$container.scrollLeft + r.clientWidth) / this.config.column_width, this.config.unit ), s = this.