// 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("
", { "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("
", { "class": "ganttview-vtheader" }); var itemDiv = jQuery("
", { "class": "ganttview-vtheader-item" }); var seriesDiv = jQuery("
", { "class": "ganttview-vtheader-series" }); for (var i = 0; i < this.data.length; i++) { var content = jQuery("") .append(jQuery("", {"class": "fa fa-info-circle tooltip", "title": this.getVerticalHeaderTooltip(this.data[i])})) .append(" "); if (this.data[i].type == "task") { content.append(jQuery("", {"href": this.data[i].link, "target": "_blank", "title": this.data[i].title}).append(this.data[i].title)); } else { content .append(jQuery("", {"href": this.data[i].board_link, "target": "_blank", "title": $(this.options.container).data("label-board-link")}).append('')) .append(" ") .append(jQuery("", {"href": this.data[i].gantt_link, "target": "_blank", "title": $(this.options.container).data("label-gantt-link")}).append('')) .append(" ") .append(jQuery("", {"href": this.data[i].link, "target": "_blank"}).append(this.data[i].title)); } seriesDiv.append(jQuery("
", {"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("
", {"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("
", { "class": "ganttview-hzheader" }); var monthsDiv = jQuery("
", { "class": "ganttview-hzheader-months" }); var daysDiv = jQuery("
", { "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("
", { "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("
", { "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("
", { "class": "ganttview-grid" }); var rowDiv = jQuery("
", { "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("
", { "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("
", { "class": "ganttview-blocks" }); for (var i = 0; i < this.data.length; i++) { blocksDiv.append(jQuery("
", { "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("
", {"class": "ganttview-block-text"}); var block = jQuery("
", { "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("
", { "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 = "" + record.column_title + " (" + record.progress + ")
" + 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("
    "); for (var user_id in record.users[type]) { list.append(jQuery("
  • ").append(record.users[type][user_id])); } tooltip += "

    " + $(this.options.container).data("label-" + type) + "

    " + 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 = "" + record.progress + "
    " + $(this.options.container).data("label-assignee") + " " + (record.assignee ? record.assignee : '') + "
    "; } tooltip += $(this.options.container).data("label-start-date") + " " + $.datepicker.formatDate('yy-mm-dd', record.start) + "
    "; 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); } };