Popular features/Context menu
Loading live demo…from dhtmlxcode.com
Popular features
Context menu
Open a custom right-click menu on a task to add a subtask, duplicate, indent or outdent, convert to a milestone, or delete it.
gantt.plugins({ tooltip: true });
gantt.message({
text: "<b>Right-click</b> a task for task actions, or right-click the grid header to show and hide columns.",
expire: -1
});
gantt.config.date_format = "%Y-%m-%d";
gantt.config.auto_types = true;
gantt.config.open_tree_initially = true;
gantt.config.grid_resize = true;
gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;
gantt.config.row_height = 34;
gantt.config.bar_height = 22;
const formatGridDate = gantt.date.date_to_str("%d %M");
const priorityColors = {
high: "#e5484d",
medium: "#f5a623",
low: "#3aa757"
};
/* ---------------------------------------------------------------------------
* DATASET
* ------------------------------------------------------------------------ */
let demoData = {
tasks: [
{ id: 1, text: "Release 4.0", type: "project", open: true },
{ id: 10, text: "Planning", type: "project", parent: 1, open: true },
{ id: 11, text: "Scope freeze", parent: 10, start_date: "2026-07-06", duration: 3, progress: 1, priority: "high" },
{ id: 12, text: "Capacity planning", parent: 10, start_date: "2026-07-09", duration: 3, progress: 0.6, priority: "medium" },
{ id: 13, text: "Stakeholder sign-off", parent: 10, start_date: "2026-07-12", duration: 2, progress: 0.3, priority: "medium" },
{ id: 14, text: "Risk assessment", parent: 10, start_date: "2026-07-13", duration: 2, progress: 0, priority: "low" },
{ id: 20, text: "Engineering", type: "project", parent: 1, open: true },
{ id: 21, text: "Feature flags", parent: 20, start_date: "2026-07-14", duration: 5, progress: 0.4, priority: "high" },
{ id: 22, text: "API schema review", parent: 20, start_date: "2026-07-16", duration: 3, progress: 0.5, priority: "medium" },
{ id: 23, text: "Perf hardening", parent: 20, start_date: "2026-07-20", duration: 6, progress: 0.1, priority: "medium" },
{ id: 24, text: "Code freeze", parent: 20, type: "milestone", start_date: "2026-07-27", progress: 0, priority: "high" },
{ id: 25, text: "Bug bash", parent: 20, start_date: "2026-07-28", duration: 3, progress: 0, priority: "low" },
{ id: 26, text: "Regression testing", parent: 20, start_date: "2026-07-29", duration: 3, progress: 0, priority: "medium" },
{ id: 30, text: "Launch", type: "project", parent: 1, open: true },
{ id: 31, text: "Release notes", parent: 30, start_date: "2026-07-30", duration: 2, progress: 0, priority: "medium" },
{ id: 32, text: "Marketing assets", parent: 30, start_date: "2026-07-31", duration: 3, progress: 0, priority: "low" },
{ id: 33, text: "Staged rollout", parent: 30, start_date: "2026-08-03", duration: 2, progress: 0, priority: "high" },
{ id: 34, text: "Ship 4.0", parent: 30, type: "milestone", start_date: "2026-08-05", progress: 0, priority: "high" },
{ id: 40, text: "Post-launch", type: "project", parent: 1, open: true },
{ id: 41, text: "Hotfix triage", parent: 40, start_date: "2026-08-06", duration: 4, progress: 0, priority: "high" },
{ id: 42, text: "Customer feedback review", parent: 40, start_date: "2026-08-10", duration: 3, progress: 0, priority: "medium" },
{ id: 43, text: "Support ticket triage", parent: 40, start_date: "2026-08-11", duration: 3, progress: 0, priority: "medium" },
{ id: 44, text: "Docs update", parent: 40, start_date: "2026-08-13", duration: 2, progress: 0, priority: "low" },
{ id: 45, text: "Usage telemetry review", parent: 40, start_date: "2026-08-14", duration: 2, progress: 0, priority: "low" },
{ id: 50, text: "Retrospective", type: "project", parent: 1, open: true },
{ id: 51, text: "Metrics review", parent: 50, start_date: "2026-08-17", duration: 2, progress: 0, priority: "medium" },
{ id: 52, text: "Survey collection", parent: 50, start_date: "2026-08-18", duration: 2, progress: 0, priority: "low" },
{ id: 53, text: "Retro meeting", parent: 50, start_date: "2026-08-19", duration: 1, progress: 0, priority: "low" },
{ id: 54, text: "Action items writeup", parent: 50, start_date: "2026-08-20", duration: 1, progress: 0, priority: "medium" },
{ id: 55, text: "Next cycle kickoff", parent: 50, type: "milestone", start_date: "2026-08-21", progress: 0, priority: "high" }
],
links: [
{ id: 1, source: 11, target: 12, type: "0" }, { id: 2, source: 12, target: 13, type: "0" },
{ id: 3, source: 13, target: 14, type: "0" }, { id: 4, source: 14, target: 21, type: "0" },
{ id: 5, source: 21, target: 22, type: "0" }, { id: 6, source: 22, target: 23, type: "0" },
{ id: 7, source: 23, target: 24, type: "0" }, { id: 8, source: 24, target: 25, type: "0" },
{ id: 9, source: 25, target: 26, type: "0" }, { id: 10, source: 26, target: 31, type: "0" },
{ id: 11, source: 31, target: 32, type: "0" }, { id: 12, source: 32, target: 33, type: "0" },
{ id: 13, source: 33, target: 34, type: "0" }, { id: 14, source: 34, target: 41, type: "0" },
{ id: 15, source: 41, target: 42, type: "0" }, { id: 16, source: 42, target: 43, type: "0" },
{ id: 17, source: 43, target: 44, type: "0" }, { id: 18, source: 44, target: 45, type: "0" },
{ id: 19, source: 45, target: 51, type: "0" }, { id: 20, source: 51, target: 52, type: "0" },
{ id: 21, source: 52, target: 53, type: "0" }, { id: 22, source: 53, target: 54, type: "0" },
{ id: 23, source: 54, target: 55, type: "0" }
]
};
/* ---------------------------------------------------------------------------
* GRID
* ------------------------------------------------------------------------ */
gantt.config.columns = [
{ name: "text", label: "Task", tree: true, width: 240, resize: true },
{ name: "priority", label: "Priority", align: "left", width: 110, resize: true, template: function (task) {
if (!task.priority || gantt.isSummaryTask(task)) {
return "";
}
return `<span class='prio'><i style='background:${priorityColors[task.priority]}'></i>${task.priority}</span>`;
}},
{name: "start_date", label: "Start", align: "center", width: 100, resize: true, template: function (task) {
return task.type === gantt.config.types.project ? "" : formatGridDate(task.start_date);
}},
{ name: "add", width: 40, resize: true }
];
gantt.config.grid_width = 480;
gantt.templates.tooltip_text = function (startDate, endDate, task) {
return "<b>" + task.text + "</b>";
};
/* ---------------------------------------------------------------------------
* SCALE
* ------------------------------------------------------------------------ */
gantt.config.scale_height = 50;
gantt.config.scales = [{ unit: "month", step: 1, format: "%F %Y" }, { unit: "day", step: 1, format: "%d" }];
/* ---------------------------------------------------------------------------
* CONTEXT MENU
* ------------------------------------------------------------------------ */
const contextMenu = document.getElementById("ctx");
const contextMenuIcons = {
taskAbove: "<polyline points='6 13 12 7 18 13'/><polyline points='6 19 12 13 18 19'/>",
taskBelow: "<polyline points='6 5 12 11 18 5'/><polyline points='6 11 12 17 18 11'/>",
childFirst: "<path d='M5 19V9a4 4 0 0 1 4-4h9'/><polyline points='14 2 18 5 14 8'/>",
childLast: "<path d='M5 5v10a4 4 0 0 0 4 4h9'/><polyline points='14 16 18 19 14 22'/>",
link: "<path d='M9 12h6'/><path d='M8 8H6a4 4 0 0 0 0 8h2'/><path d='M16 8h2a4 4 0 0 1 0 8h-2'/>",
duplicate: "<rect x='9' y='9' width='11' height='11' rx='2'/><path d='M5 15V5a2 2 0 0 1 2-2h10'/>",
indent: "<polyline points='9 8 13 12 9 16'/><line x1='4' y1='6' x2='4' y2='18'/>",
outdent: "<polyline points='15 8 11 12 15 16'/><line x1='20' y1='6' x2='20' y2='18'/>",
flag: "<line x1='5' y1='3' x2='5' y2='21'/><path d='M5 4h11l-2 4 2 4H5'/>",
check: "<polyline points='4 12 10 18 20 6'/>",
trash: "<polyline points='4 7 20 7'/><path d='M9 7V4h6v3'/><path d='M6 7l1 13h10l1-13'/>"
};
function createIcon(iconPath) {
return "<svg class='dhx_ic' viewBox='0 0 24 24'>" + iconPath + "</svg>";
}
function renderMenuItems(menuItems) {
return menuItems.map(function (menuItem, menuItemIndex) {
if (menuItem.separator) {
return "<div class='ctx__sep'></div>";
}
const leadingContent = menuItem.checkable
? "<span class='ctx__check" +
(menuItem.checked ? "" : " ctx__check--empty") +
"'>" +
createIcon(contextMenuIcons.check) +
"</span>"
: createIcon(menuItem.icon);
return "<div class='ctx__item " +
(menuItem.danger ? "danger " : "") +
(menuItem.disabled ? "disabled" : "") +
"' data-index='" +
menuItemIndex +
"'>" +
leadingContent +
menuItem.label +
"</div>";
}).join("");
}
function bindMenuItems(menuItems) {
contextMenu.querySelectorAll(".ctx__item").forEach(function (menuElement) {
menuElement.addEventListener("click", function () {
const menuItem = menuItems[
Number(menuElement.getAttribute("data-index"))
];
if (!menuItem || menuItem.disabled) {
return;
}
if (menuItem.keepOpen) {
menuItem.action();
} else {
closeMenu();
menuItem.action();
}
});
});
}
function buildTaskMenu(task) {
const isMilestone = task.type === gantt.config.types.milestone;
const isRootTask = task.parent === gantt.config.root_id || task.parent === 0 || task.parent == null;
const isCompleted = (task.progress || 0) >= 1;
const menuItems = [
{
label: "Add task above",
icon: contextMenuIcons.taskAbove,
action: function () {
addSibling(task.id, "above");
}
},
{
label: "Add task below",
icon: contextMenuIcons.taskBelow,
action: function () {
addSibling(task.id, "below");
}
},
{
label: "Add first subtask",
icon: contextMenuIcons.childFirst,
disabled: isMilestone,
action: function () {
addChild(task.id, "first");
}
},
{
label: "Add last subtask",
icon: contextMenuIcons.childLast,
disabled: isMilestone,
action: function () {
addChild(task.id, "last");
}
},
{ separator: true },
{
label: "Add successor",
icon: contextMenuIcons.link,
action: function () {
addSuccessor(task.id);
}
},
{
label: "Duplicate",
icon: contextMenuIcons.duplicate,
action: function () {
duplicateTask(task.id);
}
},
{ separator: true },
{
label: "Indent",
icon: contextMenuIcons.indent,
disabled: !canIndent(task.id),
action: function () {
indent(task.id);
}
},
{
label: "Outdent",
icon: contextMenuIcons.outdent,
disabled: isRootTask,
action: function () {
outdent(task.id);
}
},
{ separator: true },
{
label: isMilestone ? "Convert to task" : "Convert to milestone",
icon: contextMenuIcons.flag,
action: function () {
toggleMilestone(task.id);
}
},
{
label: isCompleted ? "Mark as not done" : "Mark complete",
icon: contextMenuIcons.check,
action: function () {
setDone(task.id, !isCompleted);
}
},
{ separator: true },
{
label: "Delete",
icon: contextMenuIcons.trash,
danger: true,
action: function () {
gantt.deleteTask(task.id);
}
}
];
contextMenu.innerHTML = "<div class='ctx__head'>" + DHX.ui.escape(task.text) + "</div>" + renderMenuItems(menuItems);
bindMenuItems(menuItems);
}
function getColumnLabel(column) {
if (typeof column.label === "string" && column.label) {
return column.label;
}
return column.name === "add" ? "Add" : column.name;
}
function toggleColumn(columnName) {
const column = gantt.getGridColumn(columnName);
if (!column) {
return;
}
column.hide = !column.hide;
gantt.render();
buildColumnsMenu();
positionOpenMenu();
}
function buildColumnsMenu() {
const columns = gantt.config.columns;
const visibleColumnCount = columns.filter(function (column) {
return !column.hide;
}).length;
const menuItems = columns.map(function (column) {
const isVisible = !column.hide;
return {
label: getColumnLabel(column),
checkable: true,
checked: isVisible,
disabled: isVisible && visibleColumnCount === 1,
keepOpen: true,
action: function () {
toggleColumn(column.name);
}
};
});
contextMenu.innerHTML = "<div class='ctx__head'>Grid columns</div>" + renderMenuItems(menuItems);
bindMenuItems(menuItems);
}
const menuPosition = {
left: 0,
top: 0
};
function positionOpenMenu() {
const menuRectangle = contextMenu.getBoundingClientRect();
const menuLeft = Math.max(8, Math.min(menuPosition.left, window.innerWidth - menuRectangle.width - 8));
const menuTop = Math.max(8, Math.min(menuPosition.top, window.innerHeight - menuRectangle.height - 8));
contextMenu.style.left = menuLeft + "px";
contextMenu.style.top = menuTop + "px";
}
function openTaskMenu(task, clientX, clientY) {
menuPosition.left = clientX;
menuPosition.top = clientY;
buildTaskMenu(task);
contextMenu.classList.add("open");
positionOpenMenu();
}
function openColumnsMenu(clientX, clientY) {
menuPosition.left = clientX;
menuPosition.top = clientY;
buildColumnsMenu();
contextMenu.classList.add("open");
positionOpenMenu();
}
function closeMenu() {
contextMenu.classList.remove("open");
}
gantt.attachEvent("onContextMenu", function (taskId, linkId, event) {
const gridHeader = event.target.closest(".gantt_grid_scale, .gantt_grid_head_cell");
if (gridHeader) {
event.preventDefault();
openColumnsMenu(event.clientX, event.clientY);
return false;
}
if (taskId == null || !gantt.isTaskExists(taskId)) {
return true;
}
event.preventDefault();
gantt.selectTask(taskId);
openTaskMenu(gantt.getTask(taskId), event.clientX, event.clientY);
return false;
});
document.addEventListener("mousedown", function (event) {
if (!event.target.closest(".ctx")) {
closeMenu();
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
closeMenu();
}
});
gantt.attachEvent("onGanttScroll", closeMenu);
/* ---------------------------------------------------------------------------
* ACTIONS
* ------------------------------------------------------------------------ */
function defaultTaskDate(task) {
return task.start_date ? new Date(task.start_date) : gantt.date.day_start(new Date());
}
function addSibling(taskId, position) {
const task = gantt.getTask(taskId);
const taskIndex = gantt.getTaskIndex(taskId) + (position === "below" ? 1 : 0);
gantt.createTask(
{
text: "New task",
start_date: defaultTaskDate(task),
duration: 3,
parent: task.parent
},
task.parent,
taskIndex
);
}
function addChild(parentId, position) {
const parentTask = gantt.getTask(parentId);
const childTaskIds = gantt.getChildren(parentId);
const taskIndex = position === "first" ? 0 : childTaskIds.length;
gantt.open(parentId);
gantt.createTask(
{
text: "New subtask",
start_date: defaultTaskDate(parentTask),
duration: 3,
parent: parentId
},
parentId,
taskIndex
);
}
function addSuccessor(taskId) {
const task = gantt.getTask(taskId);
const successorId = gantt.createTask(
{
text: task.text + " (next)",
start_date: task.end_date,
duration: 3,
parent: task.parent
},
task.parent
);
gantt.addLink({ source: taskId, target: successorId, type: "0"});
}
function duplicateTask(taskId) {
const task = gantt.getTask(taskId);
const taskCopy = {
id: gantt.uid(),
text: task.text + " (copy)",
start_date: task.start_date,
duration: task.duration,
type: task.type,
progress: task.progress,
priority: task.priority,
parent: task.parent
};
gantt.addTask(taskCopy, task.parent, gantt.getTaskIndex(taskId) + 1);
}
function outdent(taskId) {
const parentId = gantt.getParent(taskId);
if (parentId === gantt.config.root_id) {
return;
}
const grandParentId = gantt.getParent(parentId);
const taskIndex = gantt.getTaskIndex(parentId) + 1;
gantt.moveTask(taskId, taskIndex, grandParentId);
}
function canIndent(taskId) {
const taskIndex = gantt.getTaskIndex(taskId);
return taskIndex > 0;
}
function getPreviousSiblingParentId(taskId) {
const task = gantt.getTask(taskId);
const siblingTaskIds = gantt.getChildren(task.parent);
const taskPosition = siblingTaskIds.indexOf(taskId);
return taskPosition > 0 ? siblingTaskIds[taskPosition - 1] : task.parent;
}
function toggleMilestone(taskId) {
const task = gantt.getTask(taskId);
if (task.type === gantt.config.types.milestone) {
task.type = gantt.config.types.task;
if (!task.duration) {
task.duration = 1;
task.end_date = gantt.calculateEndDate(task);
}
} else {
if (gantt.isSummaryTask(task)) {
return;
}
task.type = gantt.config.types.milestone;
}
gantt.updateTask(taskId);
}
function setDone(taskId, isCompleted) {
const task = gantt.getTask(taskId);
task.progress = isCompleted ? 1 : 0;
gantt.updateTask(taskId);
}
function indent(taskId) {
const newParentId = getPreviousSiblingParentId(taskId);
if (newParentId === gantt.getTask(taskId).parent) {
return;
}
gantt.open(newParentId);
gantt.moveTask(taskId, -1, newParentId);
}
/* ---------------------------------------------------------------------------
* INIT
* ------------------------------------------------------------------------ */
gantt.init("gantt_here");
gantt.parse(demoData);<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Task and grid context menus: right-click actions | dhtmlxGantt</title>
<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
<script src="../common/demo-controls/dhx_controls.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<style>
body { display: flex; flex-direction: column; }
#gantt_here { width: 100%; flex: 1 1 auto; min-height: 0; }
.ctx {
position: fixed; z-index: 90; min-width: 216px;
background: var(--dhx-overlay); border: 1px solid var(--dhx-line); border-radius: 12px;
box-shadow: 0 4px 12px rgba(16,24,40,.08), 0 12px 32px rgba(16,24,40,.18);
padding: 6px; display: none;
}
.ctx.open { display: block; }
.ctx__head {
padding: 7px 10px 8px; margin-bottom: 4px; border-bottom: 1px solid var(--dhx-line-2);
font: 600 13px var(--dhx-font); color: var(--dhx-ink); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 240px;
}
.ctx__item {
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 7px;
font: 500 13px var(--dhx-font); color: var(--dhx-ink-2); cursor: pointer; white-space: nowrap;
}
.ctx__item:hover { background: var(--dhx-surface-2); color: var(--dhx-ink); }
.ctx__item .dhx_ic { width: 16px; height: 16px; color: var(--dhx-muted); }
.ctx__item:hover .dhx_ic { color: var(--dhx-accent); }
.ctx__item .ctx__check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex: none;
color: var(--dhx-accent);
}
.ctx__item .ctx__check .dhx_ic { color: currentColor; }
.ctx__item .ctx__check--empty { opacity: 0; }
.ctx__item.danger { color: var(--dhx-danger-solid); }
.ctx__item.danger:hover { background: var(--dhx-danger-bg); color: var(--dhx-danger-ink); }
.ctx__item.disabled { opacity: .4; pointer-events: none; }
.ctx__key { margin-left: auto; font: 600 11px ui-monospace, Menlo, monospace; color: var(--dhx-muted); }
.ctx__sep { height: 1px; margin: 5px 4px; background: var(--dhx-line); }
.prio { display: inline-flex; align-items: center; gap: 6px; font: 500 12.5px var(--dhx-font); color: var(--dhx-ink-2); }
.prio i { width: 8px; height: 8px; border-radius: 50%; }
</style>
</head>
<body>
<div id="gantt_here"></div>
<!-- Context menu -->
<div class="ctx" id="ctx"></div>
</body>
</html>