actually this is quit complex to give you a demo because the problem occur because of the complexity of my project. The problem I meet is quite the same as this one : http://disq.us/p/2obwc4m with the difference that I use the standard version, so I don’t want to use “gantt.destructor()” or I will not be able to parse new data without reload. Also, the problem is specific with markers (and maybe tooltip), because data, dataProcessor etc goes with the right instance.
As I can’t give you a demo, here is my complete code (sorry for that). So this is my GanttComponent, who is called one time at beginning, and then when user try another way to sort data, this component is dismounted and remounted :
<template>
<div ref="ganttContainer" style="height: 90vh; width: 99vw;"></div>
</template>
<script>
/* eslint-disable @typescript-eslint/camelcase */
import {gantt, Gantt} from 'dhtmlx-gantt';
import { getModule } from 'vuex-module-decorators';
import MainModule from '../../../../store/modules/MainModule';
import moment from 'moment-timezone';
const mainState = getModule(MainModule);
const dateToStr = gantt.date.date_to_str(gantt.config.task_date);
const dateEditor = {type: "date", map_to: "start_date"};
const durationEditor = {type: "number", map_to: "duration", min:0, max: 100};
const stateEditor = {type: "select", options: [
{key: "Unassigned", label: "Unassigned"},
{key: "Closed", label: "Closed"},
{key: "InProgress", label: "In progress"},
{key: "Late", label: "Late"},
{key: "Unscheduled", label: "Unscheduled"}
], map_to: "state"};
const legend = document.createElement('div');
legend.className = 'gantt-legend';
legend.id = 'gantt-legend';
const header = document.createElement('header');
header.className = 'legend-head';
const h3 = document.createElement('h3');
h3.textContent = 'Legend';
header.appendChild(h3);
legend.appendChild(header);
const legendList = document.createElement('div');
legendList.className = 'legend-list';
const states = ['Unassigned', 'Closed', 'In progress', 'Late', 'Unscheduled', 'folder'];
const descriptions = ['Unassigned issues', 'Closed issues', 'In progress', 'Late issues', 'Unscheduled', 'Parent'];
for (let i = 0; i < states.length; i++) {
const row = document.createElement('div');
row.className = 'legend-row';
const label = document.createElement('div');
label.className = 'legend-label ' + states[i].toLowerCase().replace(' ', '-');
const description = document.createElement('div');
description.textContent = descriptions[i];
row.appendChild(label);
row.appendChild(description);
legendList.appendChild(row);
}
legend.appendChild(legendList);
// Créer la modal et ses éléments
const modal = document.createElement('div');
const content = document.createElement('div');
const closeButton = document.createElement('span');
const text = document.createElement('p');
const okButton = document.createElement('button');
const cancelButton = document.createElement('button');
// Ajouter du texte aux éléments
closeButton.textContent = '×';
text.textContent = 'Send all changes to GitLab ?';
okButton.textContent = 'OK';
okButton.style.backgroundColor = '#4caf50';
cancelButton.textContent = 'Cancel';
cancelButton.style.backgroundColor = '#bbbbbb';
// Ajouter des classes aux éléments pour le style
modal.classList.add('modal');
content.classList.add('modal-content');
closeButton.classList.add('close-button');
// Ajouter des événements aux boutons
closeButton.addEventListener('click', () => modal.style.display = 'none');
cancelButton.addEventListener('click', () => modal.style.display = 'none');
// Ajouter les éléments à la modal
content.appendChild(closeButton);
content.appendChild(text);
content.appendChild(okButton);
content.appendChild(cancelButton);
modal.appendChild(content);
// Ajouter la modal au document
document.body.appendChild(modal);
const stateFilter = {
Unassigned: true,
Closed: true,
InProgress: true,
Late: true,
Unscheduled: true
};
let standaloneFilter = true;
const daysStyle = function(date){
const dateToStr = gantt.date.date_to_str("%D");
if (dateToStr(date) == "Sun"||dateToStr(date) == "Sat") return "weekend";
return "";
};
export default {
props: {
tasks: {
type: Object,
default () {
return {data: [], links: []}
}
},
requestsQueue: {
type: Array,
required: true
},
users: {
type: Array,
required: true
}
},
data() {
return {
selectedScale: "day",
isHidden: false,
selectedUsers: null,
markerId: null
}
},
watch: {
requestsQueue: {
handler: function (requestsQueue) {
const uploadButtonDiv = document.querySelector(".upload-logo-container");
if (requestsQueue.length > 0 && mainState.viewGateway.configuration.admin) {
if (uploadButtonDiv) {
uploadButtonDiv.style.display = 'block';
}
}
else {
if (uploadButtonDiv) {
uploadButtonDiv.style.display = 'none';
}
}
},
deep: true
},
selectedScale: {
handler: function (newVal, oldVal) {
const selectScale = document.querySelector(".selectScale");
selectScale.value = newVal;
const event = new Event('change');
selectScale.dispatchEvent(event);
},
deep: true
}
},
methods: {
$_initGanttEvents: function() {
if (!gantt.$_eventsInitialized) {
gantt.attachEvent('onTaskSelected', (id) => {
const task = gantt.getTask(id);
this.$emit('task-selected', task);
});
gantt.attachEvent('onTaskIdChange', (id, new_id) => {
if (gantt.getSelectedId() == new_id) {
const task = gantt.getTask(new_id);
this.$emit('task-selected', task);
}
});
gantt.$_eventsInitialized = true;
}
},
$_initDataProcessor: function() {
if (!gantt.$_dataProcessorInitialized) {
this.dp = gantt.createDataProcessor((entity, action, data, id) => {
this.$emit(`${entity}-updated`, id, action, data);
});
this.dp.attachEvent("onBeforeUpdate", function (id, status, data) {
if (!data.name) {
gantt.message("The task's name can't be empty!");
return false;
}
if (!data.start_date || !data.end_date || data.start_date > data.end_date) {
gantt.message("Task's dates are invalid!");
return false;
}
return true;
});
gantt.$_dataProcessorInitialized = true;
}
},
addAssignUserToTask: function(id) {
const task = gantt.getTask(id);
task.users.push('');
}
},
mounted: function () {
this.$_initGanttEvents();
const userArray = this.users.map(user => user.username);
let isThereStandaloneTasks = false;
for (const task of this.$props.tasks.data) {
if (task.level === 1 && task.parent === 0) {
isThereStandaloneTasks = true;
break;
}
}
gantt.plugins({
marker: true,
multiselect: true ,
tooltip: true ,
undo: true
});
gantt.config.undo = true;
gantt.config.redo = true;
gantt.config.date_format = "%Y-%m-%d";
gantt.config.layout = {
css: "gantt_container",
rows:[
{
html: `
<div class="gantt-controls">
<button class="gantt-undo" onclick="gantt.undo()">Undo</button>
<button class="gantt-redo" onclick="gantt.redo()">Redo</button>
<div class="state-filter">
<div class="state-filter-unassigned"><input type="checkbox" id="Unassigned" class="state-checkbox" checked=${stateFilter["Unassigned"]} onChange="stateCheckboxOnChange('Unassigned')"><label for="Unassigned">Unassigned</label></div>
<div class="state-filter-closed" style="display : ${mainState.viewGateway.configuration.addClosedIssue ? ``:`none`}"><input type="checkbox" id="Closed" class="state-checkbox" checked=${stateFilter["Closed"]} onChange="stateCheckboxOnChange('Closed')"><label for="Closed">Closed</label></div>
<div class="state-filter-inprogress"><input type="checkbox" id="InProgress" class="state-checkbox" checked=${stateFilter["InProgress"]} onChange="stateCheckboxOnChange('InProgress')"><label for="InProgress">InProgress</label></div>
<div class="state-filter-late"><input type="checkbox" id="Late" class="state-checkbox" checked=${stateFilter["Late"]} onChange="stateCheckboxOnChange('Late')"><label for="Late">Late</label></div>
<div class="state-filter-unscheduled"><input type="checkbox" id="Unscheduled" class="state-checkbox" checked=${stateFilter["Unscheduled"]} onChange="stateCheckboxOnChange('Unscheduled')"><label for="Unscheduled">Unscheduled</label></div>
</div>
<div class='searchEl'><label for="searchFilter">Search task :</label><input id='searchFilter' style='width: 120px;' type='text' placeholder='Search tasks...'></div>
${isThereStandaloneTasks ? `<div class='standaloneFilter'><label for="standaloneFilter">Standalone tasks :</label><input id='standaloneFilter' type='checkbox' checked=${standaloneFilter}></div>` : ''}
<select v-model="${this.selectedScale}" class="selectScale">
<option value="day">Day</option>
<option value="2days">2 Days</option>
<option value="week">Week</option>
<option value="month">Month</option>
</select>
<div class="custom-select-container">
<div class="custom-select" id="userSelect" ">
Select users
<div class="custom-select-arrow"></div>
</div>
<div class="custom-select-dropdown" id="userDropdown">
${this.users.map(user => `
<label class="custom-select-option">
<input type="checkbox" value="${user.username}">
${user.username}
</label>
`).join('')}
</div>
</div>
<div class="upload-logo-container" title="push all changes to GitLab" style="display: none">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<!--
<!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.>
-->
<path fill="#28bf2d" d="M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 192 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-210.7 73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-64z"/>
</svg>
</div>
</div>`
, css:"gantt-controls", height: 40
},
{ resizer: true, width: 1 },
{
cols: [
{
// the default grid view
view: "grid",
scrollX:"scrollHor",
scrollY:"scrollVer"
},
{ resizer: true, width: 1 },
{
// the default timeline view
view: "timeline",
scrollX:"scrollHor",
scrollY:"scrollVer"
},
{
view: "scrollbar",
id:"scrollVer"
}
]},
{
view: "scrollbar",
id:"scrollHor"
}
]
};
gantt.config.fit_tasks = true;
gantt.config.columns = [
{name: "name", label: "Task name", tree: true, width: 200, resize: true },
{name: "start_date", label: "Start time", align: "center", width: 150 , resize: true, editor: dateEditor},
{name: "duration", label: "Duration", align: "center", width: 60, editor: durationEditor},
{name: "users", label: "User", align: "center", width: 100, template:function(obj){
return obj.users ? obj.users.map(user => user.username).join(', ') : "";}},
{name: "state", label: "State", align: "center", width: 100, editor: stateEditor}
];
gantt.config.lightbox.sections = [
{name: "name", label: "Name", height:30, map_to:"name", type:"textarea", focus:true},
{name:"description", label: "Description", height:120, map_to:"description", type:"textarea"},
{name:"state", height:22, map_to:"state", type:"select", options: stateEditor.options},
{name:"template", height:150, type:"template", map_to:"my_template"},
{name: "time", height:72, map_to:"auto", type:"duration"}
];
gantt.config.lightbox.project_sections = [
{name: "name", label: "Name", height:30, map_to:"name", type:"textarea", focus:true},
{name:"description", label: "Description", height:72, map_to:"description", type:"textarea"},
{name:"template", height:150, type:"template", map_to:"my_template"},
{name: "time", height:72, map_to:"auto", type:"duration"}
];
gantt.locale.labels.section_name = "Title";
gantt.locale.labels.section_state = "State";
gantt.locale.labels.section_template = "Details";
gantt.config.lightbox.project_sections.allow_root = false;
gantt.attachEvent("onBeforeLightbox", (id) => {
const task = gantt.getTask(id);
if (task.level === 0 ) {
gantt.config.lightbox.sections = gantt.config.lightbox.project_sections;
task.my_template = `<div class='lightbox_labels'>${task.labels?.map(label => `<span style="padding: 3px;color: white;background-color:${label.color};border-radius:5px">${label.name}</span>`).join('')}</div>`;
return true;
}
//<!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
const addButton = `<div class='addUserAssign' ><svg xmlns="http:www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"/></svg></div>`;
task.my_template = "<span id='lightbox_users_title'>Assign to: </span><div class='lightbox_user'>"
+ `${task.users?.map((taskUser, index) => {
return `<select id="userSelect${index}" class='userSelect'> <option value='none'> </option> ${this.$props.users.map(user =>{
return `<option class="userOption" value='${JSON.stringify({ username: user.username, id: user.id })}' ${taskUser.username === user.username ? 'selected' : ''}>${user.username}</option>`
}).join('')
})} </select>`
}).join('')}`
+ addButton + "</div>"
+ "<br> <span id='lightbox_progress'>Progress: </span>"+ task.progress*100 +" %"
+ `<br> <div class='lightbox_labels'>${task.labels?.map(label => `<span style="padding: 3px;color: white;background-color:${label.color};border-radius:5px">${label.name}</span>`).join('')}</div>`;
this.$nextTick(() => {
const container = document.querySelector('.lightbox_user');
const addUserAssign = document.querySelector('.addUserAssign');
let selects = container.querySelectorAll('select');
selects.forEach(select => {
select.onchange = function(e) {
if (e.target.value === 'none') {
task.users[parseInt(select.id.split('userSelect')[1])] = '';
}
else {
task.users[parseInt(select.id.split('userSelect')[1])] = JSON.parse(e.target.value);
}
};
});
if (addUserAssign) {
addUserAssign.addEventListener('click', () => {
this.addAssignUserToTask(id);
const newUser = task.users[task.users.length - 1];
const newUserSelect = document.createElement('select');
newUserSelect.onchange = function(e) {
if (e.target.value === 'none') {
task.users[parseInt(newUserSelect.id.split('userSelect')[1])] = '';
}
else {
task.users[parseInt(newUserSelect.id.split('userSelect')[1])] = JSON.parse(e.target.value);
}
};
newUserSelect.id = `userSelect${task.users.length - 1}`;
newUserSelect.className = 'userSelect';
newUserSelect.innerHTML = `<option value='none'> </option>` + this.$props.users.map(user => `<option value='${JSON.stringify({ username: user.username, id: user.id })}'>${user.username}</option>`).join('');
const lastChild = container.lastElementChild;
container.insertBefore(newUserSelect, lastChild);
// Mettre à jour la référence à selects
selects = container.querySelectorAll('select');
});
}
});
return true;
});
gantt.config.lightbox.allow_root = false;
gantt.templates.task_text = function(start, end, task) {
return "<b>Name:</b> " + task.name ;
};
gantt.templates.tooltip_text = function(start, end, task) {
// Personnalisez ici le texte du tooltip pour chaque tâche (hover)
return "<b>" + task.name + "</b><br/>"
+ "Start: " + gantt.templates.tooltip_date_format(start) + "<br/>"
+ "End: " + gantt.templates.tooltip_date_format(end) + "<br/>"
+ "Duration: " + task.duration + " days" + "<br/>"
+ (task.state ? "<br/>State: " + task.state : "")
+ (task.users ? "<br/>" + `${task.users.map(user => user.username).join(', ')}` : "");
};
gantt.templates.task_class = function(start, end, task){
let css = "";
switch(task.state){
case "Unassigned":
css = "unassigned";
break;
case "Closed":
css = "closed";
break;
case "InProgress":
css = "in-progress";
break;
case "Late":
css = "late";
break;
case "Unscheduled":
css = "unscheduled";
break;
case gantt.hasChild(task.id):
css = "folder";
break;
default:
css = "";
}
return css;
};
gantt.templates.grid_row_class = function(start, end, task){
return task.level === 0 ? "gantt_row_project" : "";
};
gantt.config.columns_resizable = true;
gantt.config.columns_autoresize = true;
const today = new Date();
const todayMarker = gantt.addMarker({
start_date: today, //a Date object that sets the marker's date
css: "today", //a CSS class applied to the marker
text: "Now", //the marker title
title: dateToStr(today) // the marker's tooltip
});
this.markerId = gantt.getMarker(todayMarker).id;
const closestTask = this.$props.tasks.data.reduce((closest, current) => {
const currentDate = new Date(current.start_date);
const closestDate = new Date(closest.start_date);
return Math.abs(today - currentDate) < Math.abs(today - closestDate) ? current : closest;
});
const topLevelTasks = this.$props.tasks.data.filter(task => task.level === 0);
if (topLevelTasks.length < 10) {
gantt.config.open_tree_initially = true; // if less than 10 top level tasks, epics will be opened by default
}
gantt.init(this.$refs.ganttContainer);
gantt.parse(this.$props.tasks);
const start = performance.now();
gantt.sort((a, b) => {
return new Date(a.end_date) - new Date(b.end_date);
}, false, 0, false);
const end = performance.now();
const executionTime = end - start;
console.log(`Temps d'exécution tri : ${executionTime} millisecondes.`);
gantt.showDate(gantt.getMarker(todayMarker).start_date);
gantt.showTask(closestTask.id); // this will scroll the timeline to the task, horizontally and vertically
const firstElement = this.$props.tasks.data[0];
const lastElement = this.$props.tasks.data[this.$props.tasks.data.length - 1];
const startDate = new Date(firstElement.start_date);
const endDate = new Date(lastElement.end_date);
const diffInDays = moment(startDate).diff(moment(endDate), 'days');
if (diffInDays <= 7) {
this.selectedScale = "day";
} else if (diffInDays <= 30) {
this.selectedScale = "2days";
} else if (diffInDays <= 200) {
this.selectedScale = "week";
} else {
this.selectedScale = "month";
}
if (isThereStandaloneTasks) {
document.getElementById('standaloneFilter').addEventListener('change', function(e) {
standaloneFilter = e.target.checked;
gantt.refreshData();
});
}
document.querySelectorAll(".state-checkbox").forEach(function(checkbox) {
checkbox.onchange = function(e) {
stateFilter[e.target.id] = !stateFilter[e.target.id];
gantt.refreshData();
};
});
document.querySelector(".selectScale").addEventListener('change', function(e) {
this.selectedScale = e.target.value;
switch (this.selectedScale) {
case 'day':
gantt.config.scales = [
{unit: "month", step: 1, format: "%F, %Y"},
{unit: "day", step: 1, format: "%j, %D", css: daysStyle}
];
break;
case '2days':
gantt.config.scales = [
{unit: "month", step: 1, format: "%F, %Y"},
{unit: "day", step: 2, format: "%j, %D", css: daysStyle}
];
break;
case 'week':
gantt.config.scales = [
{unit: "month", step: 1, format: "%F, %Y"},
{unit: "week", step: 1, format: "%j, %D", css: daysStyle}
];
break;
case 'month':
gantt.config.scales = [
{unit: "month", step: 1, format: "%F, %Y"},
];
break;
}
gantt.render(); // re-rendre le diagramme de Gantt avec la nouvelle configuration
});
document.querySelector(".upload-logo-container").addEventListener('click', () => {
modal.style.display = 'block';
});
okButton.addEventListener('click', () => {
this.$emit('upload-tasks');
modal.style.display = 'none';
});
const userSelectBtn = document.getElementById('userSelect');
userSelectBtn.addEventListener('click', function() {
const dropdown = document.getElementById('userDropdown');
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
});
const selectUsers = document.querySelector('.custom-select-dropdown');
selectUsers.onchange = (event) => {
const selectedOptions = document.querySelectorAll('.custom-select-option input:checked');
this.selectedUsers = Array.from(selectedOptions).map(option => option.value);
// Mettre à jour l'état de selectedUsers dans votre application
gantt.refreshData();
};
document.addEventListener('click', function(event) {
const select = document.querySelector('.custom-select-container');
const dropdown = document.getElementById('userDropdown');
if (!select.contains(event.target)) {
dropdown.style.display = 'none';
}
});
let filterValue = "";
gantt.attachEvent("onDataRender", function () {
const filterEl = document.querySelector("#searchFilter")
filterEl.addEventListener('input', function (e) {
filterValue = filterEl.value;
gantt.refreshData();
});
});
const filterLogic = (task, match) => {
match = match || false;
// check children
gantt.eachTask(function (child) {
if (filterLogic(child)) {
match = true;
}
}, task.id);
// check task
if (task.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1) {
match = true;
}
// check users
if (this.selectedUsers.length > 0) {
if (task.level === 0) {
// Pour les tâches de niveau 0, vérifiez si elles ont au moins un enfant qui correspond au filtre
let hasMatchingChild = false;
gantt.eachTask((child) => {
if (child.users?.some(user => this.selectedUsers.includes(user.username))) {
hasMatchingChild = true;
}
}, task.id);
if (!hasMatchingChild) {
match = false;
}
} else if (task.level === 1 && !task.users?.some(user => this.selectedUsers.includes(user.username))) {
// Pour les tâches de niveau 1, vérifiez simplement si elles correspondent au filtre
match = false;
}
}
// check state
if (stateFilter[task.state] === false) {
match = false;
}
// check standalone
if (!standaloneFilter && !gantt.hasChild(task.id) && task.level === 1 && task.parent == 0 ) {
match = false;
}
return match;
}
gantt.attachEvent("onBeforeTaskDisplay", (id, task) => {
let thereIsAtLeastOneFilterToApply = false;
for (const state in stateFilter) {
if (!stateFilter[state]) {
thereIsAtLeastOneFilterToApply = true;
break;
}
}
let isThereUsersFilter = false;
if (this.selectedUsers && this.selectedUsers.length > 0) {
isThereUsersFilter = true;
}
if (!filterValue && !thereIsAtLeastOneFilterToApply && standaloneFilter && !isThereUsersFilter) {
return true;
}
return filterLogic(task);
});
gantt.attachEvent("onDataRender", () =>{
if (gantt.getTaskByTime().length > 0) {
const closestTask = this.$props.tasks.data.reduce((closest, current) => {
const currentDate = new Date(current.start_date);
const closestDate = new Date(closest.start_date);
return Math.abs(today - currentDate) < Math.abs(today - closestDate) ? current : closest;
});
gantt.showDate(gantt.getMarker(todayMarker).start_date);
gantt.showTask(closestTask.id);
}
});
gantt.attachEvent("onLightboxSave", function(id, item){
if(!item.name){
gantt.message({type:"error", text:"Enter task name!"});
return false;
}
if (!item.start_date || !item.end_date || item.start_date > item.end_date) {
gantt.message("Task's dates are invalid!");
return false;
}
return true;
});
gantt.$root.appendChild(legend);
this.$_initDataProcessor();
},
beforeDestroy: function() {
console.log('destroyed');
gantt.clearAll();
// gantt.deleteMarker(this.markerId);
gantt.detachAllEvents();
this.dp.destructor();
gantt.$dataProcessor = null;
gantt.$_eventsInitialized = false;
gantt.$_dataProcessorInitialized = false;
}
}
</script>
<style>