Popular features/Multiselect and indent
Loading live demo…from dhtmlxcode.com
Popular features
Multiselect and indent
Shift- or ctrl-select several rows, indent or outdent them together, and undo or redo the whole batch as one step.
gantt.plugins({ multiselect: true, undo: true, keyboard_navigation: true });
gantt.message({
text: "Hold <b>shift</b> or <b>ctrl</b> to select several rows · <b>Tab</b>/<b>Shift+Tab</b> indent/outdent · <b>Del</b> removes · <b>Ctrl+Z</b>/<b>Ctrl+Y</b> undo/redo",
expire: -1
});
gantt.config.date_format = "%Y-%m-%d";
gantt.config.row_height = 36;
gantt.config.bar_height = 24;
gantt.config.scale_height = 52;
gantt.config.order_branch = true;
gantt.config.scales = [
{ unit: "month", step: 1, format: "%F %Y" },
{ unit: "day", step: 1, format: "%j %D" }
];
gantt.config.lightbox.sections = [
{ name: "description", height: 60, map_to: "text", type: "textarea", focus: true },
{ name: "time", type: "duration", map_to: "auto" }
];
/* Selected rows + bars pick up the .gantt_selected class. */
gantt.templates.task_class =
gantt.templates.grid_row_class =
gantt.templates.task_row_class = function (start, end, task) {
return gantt.isSelectedTask(task.id) ? "gantt_selected" : "";
};
gantt.config.columns = [
{ name: "text", label: "Task", tree: true, width: 260, resize: true },
{ name: "start_date", label: "Start", align: "center", width: 100, resize: true },
{ name: "duration", label: "Days", align: "center", width: 58, resize: true },
{ name: "add", width: 40, resize: true }
];
/* --- data (id 1 already has children; the rest stays flat so indent/outdent has something to do) --- */
let data = {
tasks: [
{ id: 1, text: "Go-to-market", start_date: "2026-09-01", duration: 22, progress: 0.4, open: true, type: "project" },
{ id: 2, text: "Positioning brief", start_date: "2026-09-01", duration: 4, progress: 1, parent: 1 },
{ id: 3, text: "Messaging framework", start_date: "2026-09-05", duration: 3, progress: 1, parent: 1 },
{ id: 4, text: "Landing page copy", start_date: "2026-09-10", duration: 5, progress: 0.6, parent: 1 },
{ id: 5, text: "Landing page build", start_date: "2026-09-17", duration: 6, progress: 0.2, parent: 1 },
{ id: 6, text: "Launch email", start_date: "2026-09-17", duration: 3, progress: 0 },
{ id: 7, text: "Paid ads setup", start_date: "2026-09-22", duration: 4, progress: 0 },
{ id: 8, text: "Press kit", start_date: "2026-09-22", duration: 3, progress: 0 },
{ id: 9, text: "Webinar prep", start_date: "2026-09-24", duration: 5, progress: 0 },
{ id: 13, text: "Customer references", start_date: "2026-10-02", duration: 4, progress: 0 },
{ id: 14, text: "Community AMA", start_date: "2026-10-06", duration: 2, progress: 0 },
{ id: 20, text: "Case study drafts", start_date: "2026-09-25", duration: 5, progress: 0 },
{ id: 10, text: "Partner enablement", start_date: "2026-09-26", duration: 4, progress: 0 },
{ id: 11, text: "Sales battlecards", start_date: "2026-09-29", duration: 3, progress: 0 },
{ id: 12, text: "Analyst briefing", start_date: "2026-10-01", duration: 2, progress: 0 },
{ id: 18, text: "Social media calendar", start_date: "2026-09-18", duration: 4, progress: 0 },
{ id: 19, text: "Influencer outreach", start_date: "2026-09-23", duration: 3, progress: 0 },
{ id: 21, text: "Pricing page update", start_date: "2026-09-29", duration: 3, progress: 0 },
{ id: 22, text: "Onboarding email flow", start_date: "2026-10-01", duration: 4, progress: 0 },
{ id: 23, text: "Support docs refresh", start_date: "2026-10-03", duration: 3, progress: 0 },
{ id: 24, text: "Internal training", start_date: "2026-10-07", duration: 2, progress: 0 },
{ id: 15, text: "Launch dashboard", start_date: "2026-10-06", duration: 3, progress: 0 },
{ id: 25, text: "Metrics dashboard QA", start_date: "2026-10-08", duration: 2, progress: 0 },
{ id: 16, text: "Post-launch review", start_date: "2026-10-09", duration: 2, progress: 0 },
{ id: 26, text: "Retrospective doc", start_date: "2026-10-10", duration: 2, progress: 0 },
{ id: 17, text: "Launch day", start_date: "2026-10-13", type: "milestone" }
],
links: [
{ id: 1, source: 2, target: 3, type: "0" },
{ id: 2, source: 3, target: 4, type: "0" },
{ id: 3, source: 4, target: 5, type: "0" },
{ id: 4, source: 4, target: 6, type: "0" },
{ id: 5, source: 4, target: 8, type: "0" },
{ id: 6, source: 6, target: 7, type: "0" },
{ id: 7, source: 5, target: 9, type: "0" },
{ id: 8, source: 5, target: 10, type: "0" },
{ id: 9, source: 5, target: 18, type: "0" },
{ id: 10, source: 10, target: 11, type: "0" },
{ id: 11, source: 11, target: 12, type: "0" },
{ id: 12, source: 9, target: 13, type: "0" },
{ id: 13, source: 13, target: 14, type: "0" },
{ id: 14, source: 13, target: 20, type: "0" },
{ id: 15, source: 18, target: 19, type: "0" },
{ id: 16, source: 4, target: 21, type: "0" },
{ id: 17, source: 21, target: 22, type: "0" },
{ id: 18, source: 22, target: 23, type: "0" },
{ id: 19, source: 23, target: 24, type: "0" },
{ id: 20, source: 5, target: 15, type: "0" },
{ id: 21, source: 15, target: 16, type: "0" },
{ id: 22, source: 15, target: 25, type: "0" },
{ id: 23, source: 16, target: 26, type: "0" },
{ id: 24, source: 7, target: 17, type: "0" },
{ id: 25, source: 12, target: 17, type: "0" },
{ id: 26, source: 14, target: 17, type: "0" }
]
};
gantt.init("gantt_here");
gantt.parse(data);
function shiftTask(task_id, direction) {
let task = gantt.getTask(task_id);
task.start_date = gantt.date.add(task.start_date, direction, "day");
task.end_date = gantt.calculateEndDate(task.start_date, task.duration);
gantt.updateTask(task.id);
}
let actions = {
undo: function () { gantt.ext.undo.undo(); },
redo: function () { gantt.ext.undo.redo(); },
indent: function (task_id) {
let prev_id = gantt.getPrevSibling(task_id);
while (gantt.isSelectedTask(prev_id)) {
let prev = gantt.getPrevSibling(prev_id);
if (!prev) break;
prev_id = prev;
}
if (prev_id) {
let new_parent = gantt.getTask(prev_id);
gantt.moveTask(task_id, gantt.getChildren(new_parent.id).length, new_parent.id);
new_parent.type = gantt.config.types.project;
new_parent.$open = true;
gantt.updateTask(task_id);
gantt.updateTask(new_parent.id);
return task_id;
}
return null;
},
outdent: function (task_id, initialIndexes, initialSiblings) {
let cur_task = gantt.getTask(task_id);
let old_parent = cur_task.parent;
if (gantt.isTaskExists(old_parent) && old_parent != gantt.config.root_id) {
let index = gantt.getTaskIndex(old_parent) + 1;
let prevSibling = initialSiblings[task_id].first;
if (gantt.isSelectedTask(prevSibling)) {
index += (initialIndexes[task_id] - initialIndexes[prevSibling]);
}
gantt.moveTask(task_id, index, gantt.getParent(cur_task.parent));
if (!gantt.hasChild(old_parent))
gantt.getTask(old_parent).type = gantt.config.types.task;
gantt.updateTask(task_id);
gantt.updateTask(old_parent);
return task_id;
}
return null;
},
del: function (task_id) {
if (gantt.isTaskExists(task_id)) gantt.deleteTask(task_id);
return task_id;
},
moveForward: function (task_id) { shiftTask(task_id, 1); },
moveBackward: function (task_id) { shiftTask(task_id, -1); }
};
let cascadeAction = { indent: true, outdent: true, del: true };
let singularAction = { undo: true, redo: true };
gantt.performAction = function (actionName) {
let action = actions[actionName];
if (!action) return;
if (singularAction[actionName]) { action(); return; }
gantt.batchUpdate(function () {
let indexes = {}, siblings = {};
gantt.eachSelectedTask(function (task_id) {
gantt.ext.undo.saveState(task_id, "task");
indexes[task_id] = gantt.getTaskIndex(task_id);
siblings[task_id] = { first: null };
let currentId = task_id;
while (gantt.isTaskExists(gantt.getPrevSibling(currentId)) && gantt.isSelectedTask(gantt.getPrevSibling(currentId))) {
currentId = gantt.getPrevSibling(currentId);
}
siblings[task_id].first = currentId;
});
let updated = {};
gantt.eachSelectedTask(function (task_id) {
if (cascadeAction[actionName]) {
if (!updated[gantt.getParent(task_id)]) {
updated[action(task_id, indexes, siblings)] = true;
} else {
updated[task_id] = true;
}
} else {
action(task_id, indexes);
}
});
});
};
/* --- wire toolbar + selection counter ----------------------------------- */
function updateSel() {
DHX.ui.setText("sel_count", gantt.getSelectedTasks().length);
}
gantt.attachEvent("onTaskSelected", updateSel);
gantt.attachEvent("onTaskUnselected", updateSel);
document.querySelectorAll("[data-action]").forEach(function (btn) {
btn.addEventListener("click", function () {
gantt.performAction(this.getAttribute("data-action"));
updateSel();
});
});
/* ---------------------------------------------------------------------------
* Keyboard shortcuts.
*
* gantt.addShortcut() only fires while a task row holds real DOM focus
* *inside* the gantt container - clicking any toolbar button (they live
* outside #gantt_here) moves focus away and silently breaks every bound
* key until a row is clicked again. That's the "nothing works" symptom.
*
* Bound on `document` instead, and driven by the multiselect state
* (gantt.getSelectedTasks()) rather than a single focused row, so the keys
* work no matter what was clicked last, and Tab/Shift+Tab only take over
* while a selection is active - with nothing selected, Tab behaves like
* normal browser tab order. Tab / Shift+Tab is the standard outliner
* convention (Word, Notion, Asana) for indent/outdent, so the built-in
* Shift+Right/Left/Delete/Ctrl+Z/Ctrl+R bindings are removed to avoid
* double-firing alongside this handler.
* ------------------------------------------------------------------------ */
gantt.removeShortcut("ctrl+z");
gantt.removeShortcut("ctrl+r");
gantt.removeShortcut("shift+right", "taskRow");
gantt.removeShortcut("shift+left", "taskRow");
gantt.removeShortcut("delete", "taskRow");
function isEditableTarget(el) {
return el.closest && el.closest("input, textarea, [contenteditable], .gantt_grid_editor_placeholder, .gantt_cal_light");
}
document.addEventListener("keydown", function (e) {
if (isEditableTarget(e.target)) return;
let ctrl = e.ctrlKey || e.metaKey;
if (ctrl && !e.shiftKey && e.key.toLowerCase() === "z") {
e.preventDefault();
gantt.performAction("undo");
return;
}
if (ctrl && (e.key.toLowerCase() === "y" || (e.shiftKey && e.key.toLowerCase() === "z"))) {
e.preventDefault();
gantt.performAction("redo");
return;
}
if (!gantt.getSelectedTasks().length) return;
if (e.key === "Tab") {
e.preventDefault();
gantt.performAction(e.shiftKey ? "outdent" : "indent");
updateSel();
} else if (e.key === "Delete") {
e.preventDefault();
gantt.performAction("del");
updateSel();
}
});
/* Pre-select a task so the toolbar (indent/outdent/move/delete) is live on load. */
gantt.selectTask(5);
updateSel();<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Multiselect & indent/outdent: 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 { flex: 1 1 auto; min-height: 0; width: 100%; }
.gantt_task_line { border: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(16,24,40,.18); }
.gantt_task_line .gantt_task_progress { border-radius: 7px 0 0 7px; }
.gantt_task_line.gantt_project { border-radius: 5px; }
.gantt_task_line.gantt_selected { box-shadow: 0 0 0 2px #2563eb, 0 1px 2px rgba(16,24,40,.18); }
.gantt_row.gantt_selected, .gantt_task_row.gantt_selected {
background: var(--dhx-accent-soft) !important;
border-bottom: 1px solid rgba(37,99,235,.25);
}
.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
.hint b { color: var(--dhx-ink-2); font-weight: 600; }
.sel_pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 11px; border-radius: 999px;
background: var(--dhx-info-bg); color: var(--dhx-info-ink);
font: 600 12px var(--dhx-font);
}
kbd {
font: 600 11px var(--dhx-font);
background: var(--dhx-neutral-bg); border: 1px solid var(--dhx-line);
border-radius: 5px; padding: 1px 6px; color: var(--dhx-ink-2);
}
</style>
</head>
<body>
<div class="dhx_header" style="justify-content:center; gap:14px">
<button class="dhx_btn dhx_btn--surface" data-action="undo" title="Undo">
<svg class="dhx_ic" viewBox="0 0 16 16"><path d="M6 4 3 7l3 3"/><path d="M3 7h6a4 4 0 0 1 0 8H7"/></svg>
</button>
<button class="dhx_btn dhx_btn--surface" data-action="redo" title="Redo">
<svg class="dhx_ic" viewBox="0 0 16 16"><path d="M10 4l3 3-3 3"/><path d="M13 7H7a4 4 0 0 0 0 8h2"/></svg>
</button>
<div class="dhx_sep"></div>
<button class="dhx_btn dhx_btn--surface" data-action="outdent" title="Outdent">
<svg class="dhx_ic" viewBox="0 0 16 16"><line x1="6" y1="4" x2="14" y2="4"/><line x1="6" y1="8" x2="14" y2="8"/><line x1="6" y1="12" x2="14" y2="12"/><polyline points="4 6 2 8 4 10"/></svg>
Outdent
</button>
<button class="dhx_btn dhx_btn--surface" data-action="indent" title="Indent">
<svg class="dhx_ic" viewBox="0 0 16 16"><line x1="6" y1="4" x2="14" y2="4"/><line x1="6" y1="8" x2="14" y2="8"/><line x1="6" y1="12" x2="14" y2="12"/><polyline points="2 6 4 8 2 10"/></svg>
Indent
</button>
<div class="dhx_sep"></div>
<button class="dhx_btn dhx_btn--surface" data-action="moveBackward" title="Move 1 day earlier">← 1d</button>
<button class="dhx_btn dhx_btn--surface" data-action="moveForward" title="Move 1 day later">1d →</button>
<button class="dhx_btn dhx_btn--surface dhx_menu__item--danger" data-action="del" title="Delete">
<svg class="dhx_ic" viewBox="0 0 16 16" style="stroke:#e5484d"><polyline points="3 5 13 5"/><path d="M6 5V3h4v2"/><path d="M5 5l1 8h4l1-8"/></svg>
Delete
</button>
<span class="sel_pill"><span id="sel_count">0</span> selected</span>
</div>
<div id="gantt_here"></div>
</body>
</html>