Read-only and partial editing
What the user may do with the mouse is the editing option: false makes the chart read-only,
and an object with move, resize, progress and links turns single kinds of edit off. Two
fields of a task narrow it further: locked (no drag at all, but links may touch it) and
linkable: false (no link at all, but the bar may be dragged). Whatever the chart refuses is
reported by onEditRefused, which the line under the chart shows.
pnpm add @ganttlane/dom # vanillapnpm add @ganttlane/react react react-dom # Reactnpm install @ganttlane/dom # vanillanpm install @ganttlane/react react react-dom # Reactyarn add @ganttlane/dom # vanillayarn add @ganttlane/react react react-dom # Reactbun add @ganttlane/dom # vanillabun add @ganttlane/react react react-dom # Reactimport type { Link, Task } from '@ganttlane/core';
// 2026-03-02 is a Monday. Two kinds of task are out of the user's reach, on top of what the// `editing` option says for the whole chart:// - `locked: true` cannot be moved, resized or given progress, but links may start and end at it;// - `linkable: false` cannot be linked at all, but it can be dragged like any other bar.export const tasks: Task[] = [ // Finished work is locked: the user cannot drag it, and propagation never moves it either. { id: 'research', name: 'Research', start: '2026-03-02', end: '2026-03-06', progress: 100, locked: true, }, { id: 'design', name: 'Design', start: '2026-03-09', end: '2026-03-13', progress: 100, locked: true, }, { id: 'backend', name: 'Backend', start: '2026-03-16', end: '2026-03-27', progress: 40 }, { id: 'frontend', name: 'Frontend', start: '2026-03-16', end: '2026-03-25', progress: 25 }, // Agreed with another team: no dependency may be drawn to or from it. { id: 'legal', name: 'Legal review', start: '2026-03-18', end: '2026-03-20', progress: 0, linkable: false, }, { id: 'docs', name: 'Docs', start: '2026-03-26', end: '2026-03-27', progress: 0 }, { id: 'qa', name: 'QA', start: '2026-03-30', end: '2026-03-31', progress: 0 }, { id: 'launch', name: 'Launch', type: 'milestone', start: '2026-04-01' },];
export const links: Link[] = [ { source: 'research', target: 'design' }, { source: 'design', target: 'backend' }, { source: 'design', target: 'frontend' }, { source: 'frontend', target: 'docs' }, { source: 'backend', target: 'qa' }, { source: 'docs', target: 'qa' }, { source: 'qa', target: 'launch' },];import type { GanttOptions } from '@ganttlane/dom';
export const options = { // What the user may do with the mouse. `true` (the default) allows everything, `false` makes // the chart read-only, and an object turns single kinds of edit on and off; a flag left out // is on. The example switches it with `setOptions({ editing })`. editing: { move: true, resize: true, progress: true, links: true }, viewMode: 'week', scroll: { initial: 'first-task' }, // Smaller rows, so that all eight tasks are in view at once. layout: { rowHeight: 36, barHeight: 24 },} satisfies Partial<GanttOptions>;import '@ganttlane/dom/styles.css';
import { createGantt, type Gantt, type TaskId } from '@ganttlane/dom';
import { links, tasks } from './data';import { changeText, refusalText } from './helpers';import { options } from './options';
const FLAGS = ['move', 'resize', 'progress', 'links'] as const;
export function mount(container: HTMLElement): () => void { const controller = new AbortController(); const root = document.createElement('div'); const toolbar = document.createElement('div'); toolbar.setAttribute('role', 'toolbar'); const chart = document.createElement('div'); chart.style.height = '370px'; const status = document.createElement('p'); status.textContent = 'Drag a bar, an edge or a handle. What the chart refuses is reported here.'; root.append(toolbar, chart, status); container.append(root);
// The handlers read `gantt`, which they are passed to: give it a type. const gantt: Gantt = createGantt(chart, { tasks, links, ...options, // A drag that the chart would not start, or would not commit: nothing changed. onEditRefused: (refusal) => { status.textContent = refusalText(refusal, nameOf); }, onTasksChange: (changeSet) => { status.textContent = changeText(changeSet, nameOf); }, onLinkCreate: (link) => { status.textContent = `Linked ${nameOf(link.source)} to ${nameOf(link.target)}.`; }, }); const nameOf = (id: TaskId): string => gantt.getTask(id)?.name ?? String(id);
// One checkbox turns editing on or off as a whole (`editing: false`); the four others are its // flags. The option is replaced as a whole each time: `setOptions({ editing })`. const checkbox = (label: string): HTMLInputElement => { const input = document.createElement('input'); input.type = 'checkbox'; input.checked = true; input.addEventListener('change', apply, { signal: controller.signal }); const wrapper = document.createElement('label'); wrapper.append(input, ` ${label}`); toolbar.append(wrapper); return input; }; const master = checkbox('editing'); const boxes = FLAGS.map((flag) => ({ flag, input: checkbox(flag) })); function apply(): void { const flags = Object.fromEntries(boxes.map(({ flag, input }) => [flag, input.checked])); gantt.setOptions({ editing: master.checked ? flags : false }); for (const { input } of boxes) input.disabled = !master.checked; }
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';
import { Gantt, type GanttHandle, type TaskId } from '@ganttlane/react';import { useRef, useState } from 'react';
import { links, tasks } from './data';import { changeText, refusalText } from './helpers';import { options } from './options';
const FLAGS = ['move', 'resize', 'progress', 'links'] as const;
export default function Example() { const chart = useRef<GanttHandle>(null); const [enabled, setEnabled] = useState(true); const [flags, setFlags] = useState({ move: true, resize: true, progress: true, links: true }); const [status, setStatus] = useState( 'Drag a bar, an edge or a handle. What the chart refuses is reported here.', ); // `viewMode` is a controlled prop in React: `defaultViewMode` is where the chart starts, and // then the chart owns it. (`editing` is an ordinary prop: it is the option, as it is.) const { viewMode, ...rest } = options; const nameOf = (id: TaskId): string => chart.current?.getTask(id)?.name ?? String(id);
return ( <> <div role="toolbar"> <label> <input type="checkbox" checked={enabled} onChange={() => setEnabled(!enabled)} /> editing </label> {FLAGS.map((flag) => ( <label key={flag}> <input type="checkbox" checked={flags[flag]} disabled={!enabled} onChange={() => setFlags({ ...flags, [flag]: !flags[flag] })} />{' '} {flag} </label> ))} </div> <Gantt ref={chart} style={{ height: 370 }} // The chart owns the data (uncontrolled): these are where it starts. defaultTasks={tasks} defaultLinks={links} {...rest} defaultViewMode={viewMode} // `false` is read-only; the object turns single kinds of edit off. editing={enabled ? flags : false} // A drag that the chart would not start, or would not commit: nothing changed. onEditRefused={(refusal) => { setStatus(refusalText(refusal, nameOf)); }} onTasksChange={(changeSet) => { setStatus(changeText(changeSet, nameOf)); }} onLinkCreate={(link) => { setStatus(`Linked ${nameOf(link.source)} to ${nameOf(link.target)}.`); }} /> <p>{status}</p> </> );}import type { ChangeSet, EditRefusedPayload, TaskId } from '@ganttlane/dom';
/** What each reason of a refusal means, in words. */const REASONS: Record<EditRefusedPayload['reason'], string> = { permission: 'not allowed (the editing flags, locked, or linkable)', busy: 'the chart is busy', 'unknown-task': 'the task is gone', self: 'a task cannot depend on itself', cycle: 'the link would close a cycle', duplicate: 'the link exists',};
/** * A line for the status: what `onEditRefused` reported. * * @param refusal - The payload of the event. * @param nameOf - The name of a task from its id. * @returns The line. */export function refusalText(refusal: EditRefusedPayload, nameOf: (id: TaskId) => string): string { const { gesture, id, target, reason, at } = refusal; const what = target === undefined ? `${gesture} of ${nameOf(id)}` : `${gesture} from ${nameOf(id)} to ${nameOf(target)}`; // `at` says when the chart said no: as the drag was about to begin, or when it was let go. const when = at === 'start' ? 'at the start' : 'on release'; return `Refused (${reason}, ${when}): ${what}, ${REASONS[reason]}.`;}
/** * A line for the status: what `onTasksChange` reported. * * @param changeSet - The payload of the event. * @param nameOf - The name of a task from its id. * @returns The line. */export function changeText(changeSet: ChangeSet, nameOf: (id: TaskId) => string): string { const names = changeSet.changes.map((change) => nameOf(change.id)).join(', '); return `Changed (${changeSet.reason}): ${names}.`;}Files
data.ts: eight tasks; Research and Design arelocked, Legal review islinkable: false.options.ts: theeditingflags the chart starts with, the view mode, the first scroll and the layout.helpers.ts: the lines of the status, made from the payloads of the events.vanilla.tsandreact.tsx: the checkboxes, which callsetOptions({ editing })or pass theeditingprop.
Options
| Option | Value | What it does |
|---|---|---|
editing |
false, or { move, resize, progress, links } (all true at start) |
Read-only, or one flag per kind of edit; a flag left out is on, true is the default |
task.locked |
true on Research and Design |
The task cannot be moved, resized or given progress; links may still touch it |
task.linkable |
false on Legal review |
No link may start at or end at the task |
onEditRefused |
a function (in the example) | A drag the chart would not start or commit: { gesture, id, target, reason, at } |
onTasksChange |
a function (in the example) | A change that was made, for contrast with the refusals |
onLinkCreate |
a function (in the example) | A link that was drawn |
viewMode |
'week' |
The scale of the timeline (React: defaultViewMode) |
scroll.initial |
'first-task' |
Opens on the plan |
layout.rowHeight, .barHeight |
36, 24 |
Smaller rows, so that every task is in view |
What to try
- Untick editing and drag any bar: the chart is read-only, nothing moves, and each attempt
says
Refused (permission, at the start). Hover a bar: no handles are drawn. - Tick it again and untick resize. Drag the edge of Backend: it moves the bar, because a resize zone that is not allowed is just the body of the bar. Untick move as well and the same drag is refused.
- Research and Design are locked: dragging them is refused with every flag on. Draw a link from the round handle of Design all the same.
- Legal review has no link handles. Draw a link from Backend onto it: nothing is created, and on
release the chart reports
link-createrefused (permission). - Untick links: no handles at all. Untick progress: the little handle at the foot of a bar goes away.
Notes
- A refusal changes nothing. The payload has the
gesture('move','resize-start','resize-end','progress'or'link-create'), the taskid(the source, for a link) and thetargetof a link.atsays when:'start'as the drag was about to begin,'commit'when it was let go. Thereasonis'permission'here;'busy'while a before-hook waits, and'self','cycle'and'duplicate'for links (Link editing). - Only the user is limited.
updateTask,addLink,setTasksand the other methods ignoreeditingandlinkable; only a locked task is left alone byupdateTasktoo (it answers'unchanged'). See Programmatic updates. - A task can override the chart.
task.editabletakestrue,falseor the same four flags, and wins overediting, in both directions: a task witheditable: truecan be edited in a read-only chart.lockedandlinkable: falsestill forbid what they forbid. - Locked work stays put in propagation, too. When a predecessor moves, a locked successor does not follow, and what comes after it is not pushed either. The link is still drawn.
- React.
editingis an ordinary prop, so the checkboxes are state.viewModeis the controlled one, anddefaultViewModeis where the chart starts.