Skip to main content
Back to examples
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 &middot; <b>Tab</b>/<b>Shift+Tab</b> indent/outdent &middot; <b>Del</b> removes &middot; <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 &amp; 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">&larr; 1d</button>
	<button class="dhx_btn dhx_btn--surface" data-action="moveForward"  title="Move 1 day later">1d &rarr;</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>