481 lines
16 KiB
JavaScript
481 lines
16 KiB
JavaScript
// Based on jQuery.ganttView v.0.8.8 Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com - MIT License
|
|
Kanboard.Gantt = function(app) {
|
|
this.app = app;
|
|
this.data = [];
|
|
|
|
this.options = {
|
|
container: "#gantt-chart",
|
|
showWeekends: true,
|
|
allowMoves: true,
|
|
allowResizes: true,
|
|
cellWidth: 21,
|
|
cellHeight: 31,
|
|
slideWidth: 1000,
|
|
vHeaderWidth: 200
|
|
};
|
|
};
|
|
|
|
Kanboard.Gantt.prototype.execute = function() {
|
|
if (this.app.hasId("gantt-chart")) {
|
|
this.show();
|
|
}
|
|
};
|
|
|
|
// Save record after a resize or move
|
|
Kanboard.Gantt.prototype.saveRecord = function(record) {
|
|
this.app.showLoadingIcon();
|
|
|
|
$.ajax({
|
|
cache: false,
|
|
url: $(this.options.container).data("save-url"),
|
|
contentType: "application/json",
|
|
type: "POST",
|
|
processData: false,
|
|
data: JSON.stringify(record),
|
|
complete: this.app.hideLoadingIcon.bind(this)
|
|
});
|
|
};
|
|
|
|
// Build the Gantt chart
|
|
Kanboard.Gantt.prototype.show = function() {
|
|
this.data = this.prepareData($(this.options.container).data('records'));
|
|
|
|
var minDays = Math.floor((this.options.slideWidth / this.options.cellWidth) + 5);
|
|
var range = this.getDateRange(minDays);
|
|
var startDate = range[0];
|
|
var endDate = range[1];
|
|
var container = $(this.options.container);
|
|
var chart = jQuery("<div>", { "class": "ganttview" });
|
|
|
|
chart.append(this.renderVerticalHeader());
|
|
chart.append(this.renderSlider(startDate, endDate));
|
|
container.append(chart);
|
|
|
|
jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child", container).addClass("last");
|
|
jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child", container).addClass("last");
|
|
jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child", container).addClass("last");
|
|
|
|
if (! $(this.options.container).data('readonly')) {
|
|
this.listenForBlockResize(startDate);
|
|
this.listenForBlockMove(startDate);
|
|
}
|
|
else {
|
|
this.options.allowResizes = false;
|
|
this.options.allowMoves = false;
|
|
}
|
|
};
|
|
|
|
// Render record list on the left
|
|
Kanboard.Gantt.prototype.renderVerticalHeader = function() {
|
|
var headerDiv = jQuery("<div>", { "class": "ganttview-vtheader" });
|
|
var itemDiv = jQuery("<div>", { "class": "ganttview-vtheader-item" });
|
|
var seriesDiv = jQuery("<div>", { "class": "ganttview-vtheader-series" });
|
|
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
var content = jQuery("<span>")
|
|
.append(jQuery("<i>", {"class": "fa fa-info-circle tooltip", "title": this.getVerticalHeaderTooltip(this.data[i])}))
|
|
.append(" ");
|
|
|
|
if (this.data[i].type == "task") {
|
|
content.append(jQuery("<a>", {"href": this.data[i].link, "target": "_blank", "title": this.data[i].title}).append(this.data[i].title));
|
|
}
|
|
else {
|
|
content
|
|
.append(jQuery("<a>", {"href": this.data[i].board_link, "target": "_blank", "title": $(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>'))
|
|
.append(" ")
|
|
.append(jQuery("<a>", {"href": this.data[i].gantt_link, "target": "_blank", "title": $(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>'))
|
|
.append(" ")
|
|
.append(jQuery("<a>", {"href": this.data[i].link, "target": "_blank"}).append(this.data[i].title));
|
|
}
|
|
|
|
seriesDiv.append(jQuery("<div>", {"class": "ganttview-vtheader-series-name"}).append(content));
|
|
}
|
|
|
|
itemDiv.append(seriesDiv);
|
|
headerDiv.append(itemDiv);
|
|
|
|
return headerDiv;
|
|
};
|
|
|
|
// Render right part of the chart (top header + grid + bars)
|
|
Kanboard.Gantt.prototype.renderSlider = function(startDate, endDate) {
|
|
var slideDiv = jQuery("<div>", {"class": "ganttview-slide-container"});
|
|
var dates = this.getDates(startDate, endDate);
|
|
|
|
slideDiv.append(this.renderHorizontalHeader(dates));
|
|
slideDiv.append(this.renderGrid(dates));
|
|
slideDiv.append(this.addBlockContainers());
|
|
this.addBlocks(slideDiv, startDate);
|
|
|
|
return slideDiv;
|
|
};
|
|
|
|
// Render top header (days)
|
|
Kanboard.Gantt.prototype.renderHorizontalHeader = function(dates) {
|
|
var headerDiv = jQuery("<div>", { "class": "ganttview-hzheader" });
|
|
var monthsDiv = jQuery("<div>", { "class": "ganttview-hzheader-months" });
|
|
var daysDiv = jQuery("<div>", { "class": "ganttview-hzheader-days" });
|
|
var totalW = 0;
|
|
|
|
for (var y in dates) {
|
|
for (var m in dates[y]) {
|
|
var w = dates[y][m].length * this.options.cellWidth;
|
|
totalW = totalW + w;
|
|
|
|
monthsDiv.append(jQuery("<div>", {
|
|
"class": "ganttview-hzheader-month",
|
|
"css": { "width": (w - 1) + "px" }
|
|
}).append($.datepicker.regional[$("body").data('js-lang')].monthNames[m] + " " + y));
|
|
|
|
for (var d in dates[y][m]) {
|
|
daysDiv.append(jQuery("<div>", { "class": "ganttview-hzheader-day" }).append(dates[y][m][d].getDate()));
|
|
}
|
|
}
|
|
}
|
|
|
|
monthsDiv.css("width", totalW + "px");
|
|
daysDiv.css("width", totalW + "px");
|
|
headerDiv.append(monthsDiv).append(daysDiv);
|
|
|
|
return headerDiv;
|
|
};
|
|
|
|
// Render grid
|
|
Kanboard.Gantt.prototype.renderGrid = function(dates) {
|
|
var gridDiv = jQuery("<div>", { "class": "ganttview-grid" });
|
|
var rowDiv = jQuery("<div>", { "class": "ganttview-grid-row" });
|
|
|
|
for (var y in dates) {
|
|
for (var m in dates[y]) {
|
|
for (var d in dates[y][m]) {
|
|
var cellDiv = jQuery("<div>", { "class": "ganttview-grid-row-cell" });
|
|
if (this.options.showWeekends && this.isWeekend(dates[y][m][d])) {
|
|
cellDiv.addClass("ganttview-weekend");
|
|
}
|
|
rowDiv.append(cellDiv);
|
|
}
|
|
}
|
|
}
|
|
var w = jQuery("div.ganttview-grid-row-cell", rowDiv).length * this.options.cellWidth;
|
|
rowDiv.css("width", w + "px");
|
|
gridDiv.css("width", w + "px");
|
|
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
gridDiv.append(rowDiv.clone());
|
|
}
|
|
|
|
return gridDiv;
|
|
};
|
|
|
|
// Render bar containers
|
|
Kanboard.Gantt.prototype.addBlockContainers = function() {
|
|
var blocksDiv = jQuery("<div>", { "class": "ganttview-blocks" });
|
|
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
blocksDiv.append(jQuery("<div>", { "class": "ganttview-block-container" }));
|
|
}
|
|
|
|
return blocksDiv;
|
|
};
|
|
|
|
// Render bars
|
|
Kanboard.Gantt.prototype.addBlocks = function(slider, start) {
|
|
var rows = jQuery("div.ganttview-blocks div.ganttview-block-container", slider);
|
|
var rowIdx = 0;
|
|
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
var series = this.data[i];
|
|
var size = this.daysBetween(series.start, series.end) + 1;
|
|
var offset = this.daysBetween(start, series.start);
|
|
var text = jQuery("<div>", {"class": "ganttview-block-text"});
|
|
|
|
var block = jQuery("<div>", {
|
|
"class": "ganttview-block tooltip" + (this.options.allowMoves ? " ganttview-block-movable" : ""),
|
|
"title": this.getBarTooltip(series),
|
|
"css": {
|
|
"width": ((size * this.options.cellWidth) - 9) + "px",
|
|
"margin-left": (offset * this.options.cellWidth) + "px"
|
|
}
|
|
}).append(text);
|
|
|
|
if (size >= 2) {
|
|
text.append(series.progress);
|
|
}
|
|
|
|
block.data("record", series);
|
|
this.setBarColor(block, series);
|
|
|
|
if (series.progress != "0%") {
|
|
block.append(jQuery("<div>", {
|
|
"css": {
|
|
"z-index": 0,
|
|
"position": "absolute",
|
|
"top": 0,
|
|
"bottom": 0,
|
|
"background-color": series.color.border,
|
|
"width": series.progress,
|
|
"opacity": 0.4
|
|
}
|
|
}));
|
|
}
|
|
|
|
jQuery(rows[rowIdx]).append(block);
|
|
rowIdx = rowIdx + 1;
|
|
}
|
|
};
|
|
|
|
// Get tooltip for vertical header
|
|
Kanboard.Gantt.prototype.getVerticalHeaderTooltip = function(record) {
|
|
var tooltip = "";
|
|
|
|
if (record.type == "task") {
|
|
tooltip = "<strong>" + record.column_title + "</strong> (" + record.progress + ")<br/>" + record.title;
|
|
}
|
|
else {
|
|
var types = ["managers", "members"];
|
|
|
|
for (var index in types) {
|
|
var type = types[index];
|
|
if (! jQuery.isEmptyObject(record.users[type])) {
|
|
var list = jQuery("<ul>");
|
|
|
|
for (var user_id in record.users[type]) {
|
|
list.append(jQuery("<li>").append(record.users[type][user_id]));
|
|
}
|
|
|
|
tooltip += "<p><strong>" + $(this.options.container).data("label-" + type) + "</strong></p>" + list[0].outerHTML;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tooltip;
|
|
};
|
|
|
|
// Get tooltip for bars
|
|
Kanboard.Gantt.prototype.getBarTooltip = function(record) {
|
|
var tooltip = "";
|
|
|
|
if (record.not_defined) {
|
|
tooltip = $(this.options.container).data("label-not-defined");
|
|
}
|
|
else {
|
|
if (record.type == "task") {
|
|
tooltip = "<strong>" + record.progress + "</strong><br/>" +
|
|
$(this.options.container).data("label-assignee") + " " + (record.assignee ? record.assignee : '') + "<br/>";
|
|
}
|
|
|
|
tooltip += $(this.options.container).data("label-start-date") + " " + $.datepicker.formatDate('yy-mm-dd', record.start) + "<br/>";
|
|
tooltip += $(this.options.container).data("label-end-date") + " " + $.datepicker.formatDate('yy-mm-dd', record.end);
|
|
}
|
|
|
|
return tooltip;
|
|
};
|
|
|
|
// Set bar color
|
|
Kanboard.Gantt.prototype.setBarColor = function(block, record) {
|
|
if (record.not_defined) {
|
|
block.addClass("ganttview-block-not-defined");
|
|
}
|
|
else {
|
|
block.css("background-color", record.color.background);
|
|
block.css("border-color", record.color.border);
|
|
}
|
|
};
|
|
|
|
// Setup jquery-ui resizable
|
|
Kanboard.Gantt.prototype.listenForBlockResize = function(startDate) {
|
|
var self = this;
|
|
|
|
jQuery("div.ganttview-block", this.options.container).resizable({
|
|
grid: this.options.cellWidth,
|
|
handles: "e,w",
|
|
delay: 300,
|
|
stop: function() {
|
|
var block = jQuery(this);
|
|
self.updateDataAndPosition(block, startDate);
|
|
self.saveRecord(block.data("record"));
|
|
}
|
|
});
|
|
};
|
|
|
|
// Setup jquery-ui drag and drop
|
|
Kanboard.Gantt.prototype.listenForBlockMove = function(startDate) {
|
|
var self = this;
|
|
|
|
jQuery("div.ganttview-block", this.options.container).draggable({
|
|
axis: "x",
|
|
delay: 300,
|
|
grid: [this.options.cellWidth, this.options.cellWidth],
|
|
stop: function() {
|
|
var block = jQuery(this);
|
|
self.updateDataAndPosition(block, startDate);
|
|
self.saveRecord(block.data("record"));
|
|
}
|
|
});
|
|
};
|
|
|
|
// Update the record data and the position on the chart
|
|
Kanboard.Gantt.prototype.updateDataAndPosition = function(block, startDate) {
|
|
var container = jQuery("div.ganttview-slide-container", this.options.container);
|
|
var scroll = container.scrollLeft();
|
|
var offset = block.offset().left - container.offset().left - 1 + scroll;
|
|
var record = block.data("record");
|
|
|
|
// Restore color for defined block
|
|
record.not_defined = false;
|
|
this.setBarColor(block, record);
|
|
|
|
// Set new start date
|
|
var daysFromStart = Math.round(offset / this.options.cellWidth);
|
|
var newStart = this.addDays(this.cloneDate(startDate), daysFromStart);
|
|
record.start = newStart;
|
|
|
|
// Set new end date
|
|
var width = block.outerWidth();
|
|
var numberOfDays = Math.round(width / this.options.cellWidth) - 1;
|
|
record.end = this.addDays(this.cloneDate(newStart), numberOfDays);
|
|
|
|
if (record.type === "task" && numberOfDays > 0) {
|
|
jQuery("div.ganttview-block-text", block).text(record.progress);
|
|
}
|
|
|
|
// Update tooltip
|
|
block.attr("title", this.getBarTooltip(record));
|
|
|
|
block.data("record", record);
|
|
|
|
// Remove top and left properties to avoid incorrect block positioning,
|
|
// set position to relative to keep blocks relative to scrollbar when scrolling
|
|
block
|
|
.css("top", "")
|
|
.css("left", "")
|
|
.css("position", "relative")
|
|
.css("margin-left", offset + "px");
|
|
};
|
|
|
|
// Creates a 3 dimensional array [year][month][day] of every day
|
|
// between the given start and end dates
|
|
Kanboard.Gantt.prototype.getDates = function(start, end) {
|
|
var dates = [];
|
|
dates[start.getFullYear()] = [];
|
|
dates[start.getFullYear()][start.getMonth()] = [start];
|
|
var last = start;
|
|
|
|
while (this.compareDate(last, end) == -1) {
|
|
var next = this.addDays(this.cloneDate(last), 1);
|
|
|
|
if (! dates[next.getFullYear()]) {
|
|
dates[next.getFullYear()] = [];
|
|
}
|
|
|
|
if (! dates[next.getFullYear()][next.getMonth()]) {
|
|
dates[next.getFullYear()][next.getMonth()] = [];
|
|
}
|
|
|
|
dates[next.getFullYear()][next.getMonth()].push(next);
|
|
last = next;
|
|
}
|
|
|
|
return dates;
|
|
};
|
|
|
|
// Convert data to Date object
|
|
Kanboard.Gantt.prototype.prepareData = function(data) {
|
|
for (var i = 0; i < data.length; i++) {
|
|
var start = new Date(data[i].start[0], data[i].start[1] - 1, data[i].start[2], 0, 0, 0, 0);
|
|
data[i].start = start;
|
|
|
|
var end = new Date(data[i].end[0], data[i].end[1] - 1, data[i].end[2], 0, 0, 0, 0);
|
|
data[i].end = end;
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
// Get the start and end date from the data provided
|
|
Kanboard.Gantt.prototype.getDateRange = function(minDays) {
|
|
var minStart = new Date();
|
|
var maxEnd = new Date();
|
|
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
var start = new Date();
|
|
start.setTime(Date.parse(this.data[i].start));
|
|
|
|
var end = new Date();
|
|
end.setTime(Date.parse(this.data[i].end));
|
|
|
|
if (i == 0) {
|
|
minStart = start;
|
|
maxEnd = end;
|
|
}
|
|
|
|
if (this.compareDate(minStart, start) == 1) {
|
|
minStart = start;
|
|
}
|
|
|
|
if (this.compareDate(maxEnd, end) == -1) {
|
|
maxEnd = end;
|
|
}
|
|
}
|
|
|
|
// Insure that the width of the chart is at least the slide width to avoid empty
|
|
// whitespace to the right of the grid
|
|
if (this.daysBetween(minStart, maxEnd) < minDays) {
|
|
maxEnd = this.addDays(this.cloneDate(minStart), minDays);
|
|
}
|
|
|
|
// Always start one day before the minStart
|
|
minStart.setDate(minStart.getDate() - 1);
|
|
|
|
return [minStart, maxEnd];
|
|
};
|
|
|
|
// Returns the number of day between 2 dates
|
|
Kanboard.Gantt.prototype.daysBetween = function(start, end) {
|
|
if (! start || ! end) {
|
|
return 0;
|
|
}
|
|
|
|
var count = 0, date = this.cloneDate(start);
|
|
|
|
while (this.compareDate(date, end) == -1) {
|
|
count = count + 1;
|
|
this.addDays(date, 1);
|
|
}
|
|
|
|
return count;
|
|
};
|
|
|
|
// Return true if it's the weekend
|
|
Kanboard.Gantt.prototype.isWeekend = function(date) {
|
|
return date.getDay() % 6 == 0;
|
|
};
|
|
|
|
// Clone Date object
|
|
Kanboard.Gantt.prototype.cloneDate = function(date) {
|
|
return new Date(date.getTime());
|
|
};
|
|
|
|
// Add days to a Date object
|
|
Kanboard.Gantt.prototype.addDays = function(date, value) {
|
|
date.setDate(date.getDate() + value * 1);
|
|
return date;
|
|
};
|
|
|
|
/**
|
|
* Compares the first date to the second date and returns an number indication of their relative values.
|
|
*
|
|
* -1 = date1 is lessthan date2
|
|
* 0 = values are equal
|
|
* 1 = date1 is greaterthan date2.
|
|
*/
|
|
Kanboard.Gantt.prototype.compareDate = function(date1, date2) {
|
|
if (isNaN(date1) || isNaN(date2)) {
|
|
throw new Error(date1 + " - " + date2);
|
|
} else if (date1 instanceof Date && date2 instanceof Date) {
|
|
return (date1 < date2) ? -1 : (date1 > date2) ? 1 : 0;
|
|
} else {
|
|
throw new TypeError(date1 + " - " + date2);
|
|
}
|
|
};
|