feat: Integrate JS Gantt Chart library and update styles
- Added JS Gantt Chart library files including CSS and JS. - Updated existing Gantt chart styles for better visual representation. - Translated month and day names to Polish in the Gantt chart settings. - Implemented Gantt chart initialization in the main view with sample data. - Added a toggle switch for displaying tasks in the calendar. - Enhanced task edit form with a new checkbox for calendar visibility. - Improved the layout of the logged-in user template to include Gantt chart styles and scripts.
This commit is contained in:
399
libraries/JS-Gantt-Chart-master/gantt.js
Normal file
399
libraries/JS-Gantt-Chart-master/gantt.js
Normal file
@@ -0,0 +1,399 @@
|
||||
class Gantt {
|
||||
constructor(staticID, params) {
|
||||
this.staticID = staticID;
|
||||
this.sidebarHeader = params.sidebarHeader || 'Unused parameter right now';
|
||||
this.noDataFoundMessage = params.noDataFoundMessage || 'No data found.';
|
||||
this.startTimeAlias = params.startTimeAlias || 'startTime';
|
||||
this.endTimeAlias = params.endTimeAlias || 'endTime';
|
||||
this.idAlias = params.idAlias || 'id';
|
||||
this.rowAlias = params.rowAlias || 'rowTitle';
|
||||
this.linkAlias = params.linkAlias;
|
||||
this.tooltipAlias = params.tooltipAlias || 'tooltip';
|
||||
this.groupBy = params.groupBy ? params.groupBy.split(',').map(group => group.trim()) : [];
|
||||
this.groupByAlias = this.groupBy ? (params.groupByAlias ? params.groupByAlias.split(',').map(group => group.trim()) : this.groupBy) : [];
|
||||
this.data = {};
|
||||
this.rawData = [];
|
||||
this.divisionCount = 0;
|
||||
this.maxTime;
|
||||
this.minTime;
|
||||
this.refreshFunction = params.refreshFunction;
|
||||
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.classList.add('gfb-gantt-wrapper');
|
||||
document.getElementById(this.staticID).appendChild(this.wrapper);
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.rawData = this.refreshFunction();
|
||||
this.empty();
|
||||
if(this.rawData.length < 1){
|
||||
this.noDataFound();
|
||||
return;
|
||||
}
|
||||
this.processData();
|
||||
this.render();
|
||||
}
|
||||
|
||||
processData() {
|
||||
//Have to reset the divisionCount here. Otherwise it keeps incrementing on the divisionCount of the previous data set.
|
||||
this.divisionCount = 0;
|
||||
|
||||
//Sorts the data by the start time ascending before anything else is done
|
||||
//This ensures that the data is rendered in the proper location when we generate the chart.
|
||||
let sortedData = this.rawData.sort((a, b) => new Date(a[this.startTimeAlias]) - new Date(b[this.startTimeAlias])),
|
||||
groupedData = {};
|
||||
for (let dataRow of sortedData) {
|
||||
this.groupArray(groupedData, dataRow);
|
||||
//console.log(groupedData);
|
||||
}
|
||||
|
||||
this.data = groupedData;
|
||||
|
||||
//Get the max and min datetimes in the dataset
|
||||
//Used to determine the first and last times that are shown in the chart
|
||||
let maxTime = this.rawData.reduce((max, curr) => !!curr[this.endTimeAlias] ? max > new Date(curr[this.endTimeAlias]) ? max : new Date(curr[this.endTimeAlias]) : max, new Date(0));
|
||||
let minTime = this.rawData.reduce((min, curr) => !!curr[this.startTimeAlias] ? min > new Date(curr[this.startTimeAlias]) ? new Date(curr[this.startTimeAlias]) : min : min, new Date(maxTime.getTime()));
|
||||
this.maxTime = roundHourUp(maxTime);
|
||||
this.minTime = roundHourDown(minTime);
|
||||
|
||||
if(this.minTime === this.maxTime)
|
||||
return;
|
||||
|
||||
//Determines the number of columns(times) that will be shown in the chart.
|
||||
//Mostly used for iteration later in the render process
|
||||
for (let i = new Date(this.minTime.getTime()); i <= this.maxTime; i.setTime(i.getTime() + (60 * 60 * 1000))) {
|
||||
this.divisionCount++;
|
||||
}
|
||||
|
||||
}
|
||||
//Takes advantage of the fact that javascript is kind of pass-by-reference for object data types
|
||||
//So every change to result here is actually a change to our groupedData variable
|
||||
groupArray(result, entry, iter = 0) {
|
||||
let nextResult;
|
||||
if (iter === this.groupBy.length + 1) {
|
||||
result.push(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let groupingProperty = result[entry[this.groupBy[iter]]];
|
||||
let chartEntry = result[entry[this.idAlias]];
|
||||
|
||||
//If we can't find a property within result for the groupBy parameter at this level we create a new one.
|
||||
if (!result[entry[this.groupBy[iter]]] && iter < this.groupBy.length) {
|
||||
result[entry[this.groupBy[iter]]] = {groupName: entry[this.groupByAlias[iter]]};
|
||||
nextResult = result[entry[this.groupBy[iter]]];
|
||||
}
|
||||
else if (!result[entry[this.idAlias]] && iter === this.groupBy.length) {
|
||||
result[entry[this.idAlias]] = [];
|
||||
nextResult = result[entry[this.idAlias]];
|
||||
}
|
||||
else if (result[entry[this.groupBy[iter]]]) {
|
||||
nextResult = result[entry[this.groupBy[iter]]];
|
||||
}
|
||||
else if (result[entry[this.idAlias]]) {
|
||||
nextResult = result[entry[this.idAlias]];
|
||||
}
|
||||
iter++;
|
||||
this.groupArray(nextResult, entry, iter);
|
||||
}
|
||||
|
||||
//Creates the header for the Gantt chart
|
||||
buildHeader() {
|
||||
let headerDivs = `<div class="gfb-gantt-header-spacer"></div>`;
|
||||
if (this.divisionCount > 1) {
|
||||
for (let i = 0; i < this.divisionCount; i++) {
|
||||
let date = new Date(this.minTime.getTime() + ((60 * 60 * 1000) * i)),
|
||||
hour = date.getHours().toString().padStart(2, '0'),
|
||||
minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
headerDivs += `<div class="gfb-gantt-header">${hour}:${minutes}</div>`;
|
||||
}
|
||||
}
|
||||
return `<div class="gfb-gantt-headers" style="grid-template-columns: 100px repeat(${this.divisionCount}, 1fr)">${headerDivs}</div>`;
|
||||
}
|
||||
|
||||
|
||||
buildLines() {
|
||||
let lines = '<div class="gfb-gantt-sidebar-template"></div>';
|
||||
for (let i = 0; i < this.divisionCount; i++) {
|
||||
lines += `<div class="gfb-gantt-line"></div>`;
|
||||
}
|
||||
return `<div class="gfb-gantt-lines-container" style="grid-template-columns: 100px repeat(${this.divisionCount}, 1fr)">${lines}</div>`;
|
||||
}
|
||||
|
||||
buildRow(rowArr, dataIndex) {
|
||||
let totalTime = this.maxTime - this.minTime,
|
||||
compositeRows = `<div style="grid-column: 2/${this.divisionCount + 1};grid-row:1;display:flex;align-items:center"><div class="gfb-gantt-sub-row-wrapper">`;
|
||||
for (let i = 0; i < rowArr.length; i++) {
|
||||
//Check to see if the current entry has a start and end time. If not we break
|
||||
if (!rowArr[i][this.startTimeAlias] || !rowArr[i][this.startTimeAlias])
|
||||
break;
|
||||
let currElStart = new Date(rowArr[i][this.startTimeAlias]),
|
||||
currElEnd = new Date(rowArr[i][this.endTimeAlias]),
|
||||
currElRunPercent = ((currElEnd - currElStart) / totalTime) * 100;
|
||||
if (i === 0 || (rowArr[i - 1] && new Date(rowArr[i - 1][this.endTimeAlias]) !== currElStart)) {
|
||||
let baseTime = (i === 0 ? this.minTime : new Date(rowArr[i - 1][this.endTimeAlias])),
|
||||
difference = ((currElStart - baseTime) / totalTime) * 100;
|
||||
compositeRows += `<div style="width:${difference}%;"></div>`;
|
||||
}
|
||||
//If we don't have a linkAlias we assume the entries are not meant to link anywhere, so we just render them as divs instead.
|
||||
if (this.linkAlias)
|
||||
compositeRows += `<a class="gfb-gantt-row-entry" style="width:${currElRunPercent}%;" href="${rowArr[i][this.linkAlias]}" data-index="${dataIndex.join('-')}-${i}"></a>`;
|
||||
else
|
||||
compositeRows += `<div class="gfb-gantt-row-entry" style="width:${currElRunPercent}%;" data-index="${dataIndex.join('-')}-${i}"></div>`;
|
||||
}
|
||||
return compositeRows + '</div></div>';
|
||||
}
|
||||
|
||||
buildContent() {
|
||||
let body = ['<div class="gfb-gantt-row-container">'],
|
||||
header = this.buildHeader(),
|
||||
this1 = this;
|
||||
|
||||
buildContent(this.data, body);
|
||||
body.push('</div>');
|
||||
|
||||
return `<div class="gfb-gantt-content">${header}${body.join('')}</div>`;
|
||||
|
||||
function buildContent(data, result, depth = 0, dataIndex = []) {
|
||||
for (let prop in data) {
|
||||
if (data[prop]) {
|
||||
if(prop !== 'groupName')
|
||||
dataIndex.push(prop);
|
||||
|
||||
if (typeof data[prop] === "object" && !Array.isArray(data[prop])) {
|
||||
result.push(`<div class="gfb-gantt-grouping-header" style="padding-left: ${5 + (depth * 20)}px">${data[prop].groupName}</div><div>`);
|
||||
depth += 1;
|
||||
buildContent(data[prop], result, depth, dataIndex);
|
||||
depth -= 1;
|
||||
}
|
||||
else if (Array.isArray(data[prop])) {
|
||||
result.push(`<div class="gfb-gantt-row" style="grid-template-columns: 100px repeat(${this1.divisionCount}, 1fr)"><div class="gfb-gantt-sidebar-header">${data[prop][0][this1.rowAlias]}</div>${this1.buildRow(data[prop], dataIndex)}</div>`);
|
||||
}
|
||||
dataIndex.pop();
|
||||
}
|
||||
}
|
||||
result.push('</div>');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildChart() {
|
||||
let content = this.buildContent();
|
||||
let lines = this.buildLines();
|
||||
return `${lines}${content}`;
|
||||
}
|
||||
|
||||
bindHover() {
|
||||
let bindElements = document.querySelectorAll(`#${this.staticID} .gfb-gantt-row-entry`);
|
||||
|
||||
bindElements.forEach(bindElement => {
|
||||
let toolTipElement,
|
||||
timeout;
|
||||
bindElement.addEventListener('mouseover', e => {
|
||||
let target = e.target,
|
||||
indexArray = target.getAttribute('data-index').split('-'),
|
||||
position = getElOffset(target),
|
||||
targetHeight = getBoundingRect(target).height,
|
||||
this1 = this;
|
||||
indexArray.push(this.tooltipAlias);
|
||||
position.top += (targetHeight + 5);
|
||||
target.classList.add('hovering');
|
||||
//Set a delay on the tooltip appearing so that it doesn't immediately appear if the user happens to swipe their mouse across the screen.
|
||||
//At the end of the delay we check to see if the mouse is still hovering, at which point we will show the tooltip
|
||||
timeout = setTimeout(() => {
|
||||
if (!target.classList.contains('hovering'))
|
||||
return;
|
||||
toolTipElement = document.createElement(`div`);
|
||||
toolTipElement.classList.add('gfb-gantt-row-entry-tooltip');
|
||||
toolTipElement.innerHTML = getToolTipData();
|
||||
toolTipElement.style.top = `${position.top}px`;
|
||||
toolTipElement.style.left = `${position.left}px`;
|
||||
document.body.appendChild(toolTipElement);
|
||||
fadeIn(toolTipElement, 300);
|
||||
}, 300);
|
||||
|
||||
function getToolTipData() {
|
||||
let dataArray = this1.data;
|
||||
for (let i = 0; i < indexArray.length; i++) {
|
||||
if (i === indexArray.length - 1)
|
||||
return dataArray[indexArray[i]]
|
||||
else
|
||||
dataArray = dataArray[indexArray[i]];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bindElement.addEventListener('mouseout', e => {
|
||||
let target = e.target;
|
||||
|
||||
clearTimeout(timeout);
|
||||
target.classList.remove('hovering');
|
||||
if (toolTipElement)
|
||||
fadeOut(toolTipElement, 300, () => toolTipElement.remove());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
bindCollapse(){
|
||||
let bindElements = document.querySelectorAll(`#${this.staticID} .gfb-gantt-grouping-header`);
|
||||
|
||||
bindElements.forEach(bindElement => {
|
||||
bindElement.addEventListener('click', e => {
|
||||
let group = e.target.nextSibling,
|
||||
duration = 300;
|
||||
if(!group.classList.contains('gfb-gantt-group--collapsed')){
|
||||
slideUp(group, duration);
|
||||
group.classList.add('gfb-gantt-group--collapsed');
|
||||
}
|
||||
else{
|
||||
slideDown(group, duration);
|
||||
group.classList.remove('gfb-gantt-group--collapsed');
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper.innerHTML = this.buildChart();
|
||||
|
||||
this.bindCollapse();
|
||||
|
||||
if (this.tooltipAlias)
|
||||
this.bindHover();
|
||||
}
|
||||
|
||||
noDataFound() {
|
||||
this.wrapper.innerHTML = this.noDataFoundMessage;
|
||||
}
|
||||
|
||||
empty() {
|
||||
this.wrapper.innerHTML = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function roundHourUp(date) {
|
||||
let m = 60 * 60 * 1000
|
||||
return new Date(Math.ceil(date.getTime() / m) * m);
|
||||
}
|
||||
function roundHourDown(date) {
|
||||
let m = 60 * 60 * 1000;
|
||||
return new Date(Math.floor(date.getTime() / m) * m);
|
||||
}
|
||||
|
||||
function fadeIn(element, ms, callback = null) {
|
||||
let duration = ms,
|
||||
interval = 50,
|
||||
gap = interval / duration,
|
||||
opacity = 0;
|
||||
|
||||
element.style.display = 'block';
|
||||
element.style.opacity = opacity;
|
||||
|
||||
let fading = window.setInterval(fade, interval);
|
||||
|
||||
function fade() {
|
||||
opacity = opacity + gap;
|
||||
element.style.opacity = opacity;
|
||||
|
||||
if (opacity <= 0)
|
||||
element.style.display = 'none';
|
||||
if (opacity >= 1) {
|
||||
window.clearInterval(fading);
|
||||
if (callback)
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
function fadeOut(element, ms, callback = null) {
|
||||
let duration = ms,
|
||||
interval = 50,
|
||||
gap = interval / duration,
|
||||
opacity = 1;
|
||||
|
||||
let fading = window.setInterval(fade, interval);
|
||||
|
||||
function fade() {
|
||||
opacity = opacity - gap;
|
||||
element.style.opacity = opacity;
|
||||
|
||||
if (opacity <= 0)
|
||||
element.style.display = 'none';
|
||||
if (opacity <= 0) {
|
||||
window.clearInterval(fading);
|
||||
if (callback)
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getElOffset(element) {
|
||||
let boundingRect = getBoundingRect(element);
|
||||
return { top: boundingRect.top + window.scrollY, left: boundingRect.left + window.scrollX };
|
||||
}
|
||||
function getBoundingRect(element) {
|
||||
return element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function slideUp(element, ms){
|
||||
|
||||
setProperty(element, 'height', `${element.offsetHeight}px`);
|
||||
setProperty(element, 'transition-property', 'height, margin, padding');
|
||||
setProperty(element, 'transition-duration', `${ms}ms`);
|
||||
setProperty(element, 'box-sizing', 'border-box');
|
||||
|
||||
zeroMultiProperty(element, ['margin-bottom', 'margin-top', 'padding-top', 'padding-bottom']);
|
||||
setProperty(element, 'overflow', 'hidden');
|
||||
|
||||
//Need a timeout otherwise the slide up animation is not performed
|
||||
setTimeout(() => setProperty(element, 'height'), 0);
|
||||
|
||||
setTimeout(() => {
|
||||
setProperty(element, 'display', 'none');
|
||||
removeMultiProperty(element, ['height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'overflow', 'transition-duration', 'transition-property'])
|
||||
}, ms);
|
||||
}
|
||||
function slideDown(element, ms){
|
||||
|
||||
element.style.display = 'block';
|
||||
|
||||
let height = element.offsetHeight;
|
||||
|
||||
setProperty(element, 'height');
|
||||
setProperty(element, 'transition-property', 'height, margin, padding');
|
||||
setProperty(element, 'transition-duration', `${ms}ms`);
|
||||
setProperty(element, 'box-sizing', 'border-box');
|
||||
setProperty(element, 'overflow', 'hidden');
|
||||
|
||||
//Need a timeout otherwise the slide up animation is not performed
|
||||
setTimeout(() => setProperty(element, 'height', `${height}px`), 0);
|
||||
|
||||
setTimeout(() => {
|
||||
removeMultiProperty(element, ['height', 'overflow', 'transition-duration', 'transition-property']);
|
||||
}, ms);
|
||||
}
|
||||
|
||||
//Sets the inline property of the given element to the value provided.
|
||||
//If not provided, the value is set to 0
|
||||
function setProperty(element, property, value = 0) {
|
||||
element.style.setProperty(property, value);
|
||||
}
|
||||
|
||||
//Iteratively calls setProperty
|
||||
function zeroMultiProperty(element, properties) {
|
||||
for (property of properties) {
|
||||
setProperty(element, property);
|
||||
}
|
||||
}
|
||||
|
||||
//Iteratively calls removeProperty
|
||||
function removeMultiProperty(element, properties){
|
||||
for(property of properties)
|
||||
removeProperty(element, property);
|
||||
}
|
||||
//Removes the given property from the given element's inline styling
|
||||
function removeProperty(element, property){
|
||||
element.style.removeProperty(property);
|
||||
};
|
||||
Reference in New Issue
Block a user