Skip to content

Calendars and Duration

Open View → Working calendars in the Gantt toolbar below to open the calendar editor. The live example includes the editor plugin and its opt-in toolbar action; expand Source code to see the integration in TypeScript, Vue, React, or Angular.

Source code
TypeScriptts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return () => {};
  const root = document.createElement('section');
  const controls = document.createElement('div');
  const status = document.createElement('p');
  const toolbar = document.createElement('div');
  const grid = document.createElement('revo-grid');
  const persistence = createProductionPersistence();
  let readOnly = false;
  let localized = false;
  let large = false;
  let project: ProjectSnapshot | null = null;
  const applyConfig = () => { grid.gantt = createProductionGanttConfig(readOnly, localized, project); };
  const applyProject = () => {
    applyConfig();
    grid.ganttDependencies = project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES);
    grid.ganttCalendars = project?.calendars ?? [STANDARD_CALENDAR];
    grid.ganttResources = project?.resources ?? SHOWCASE_RESOURCES;
    grid.ganttAssignments = project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS);
    grid.ganttBaselines = project?.baselines ?? (large ? [] : SHOWCASE_BASELINES);
    grid.source = project ? productionSourceRows(project) : createProductionTasks(large);
  };
  const toggle = (label: string, update: (value: boolean) => void) => {
    const element = document.createElement('label');
    const input = document.createElement('input');
    input.type = 'checkbox';
    input.addEventListener('change', () => update(input.checked));
    element.append(input, document.createTextNode(label));
    return { element, input };
  };
  const run = async (action: () => unknown | Promise<unknown>) => {
    try { await action(); } catch (error) { status.textContent = productionError(error); }
  };
  const button = (label: string, action: () => unknown | Promise<unknown>) => {
    const element = document.createElement('button');
    element.type = 'button';
    element.textContent = label;
    element.className = 'rv-btn';
    element.addEventListener('click', () => { void run(action); });
    return element;
  };
  root.className = 'gantt-production-demo';
  controls.className = 'gantt-production-controls';
  status.className = 'gantt-production-status';
  status.setAttribute('role', 'status');
  status.textContent = 'Save and restore use browser memory only; no server is contacted.';
  toolbar.className = 'gantt-production-toolbar';
  grid.className = 'gantt-production-grid';
  grid.theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  grid.hideAttribution = true;
  grid.plugins = [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin];
  grid.rowSelect = { rowOrder: true };
  grid.columns = [...SHOWCASE_COLUMNS_WITH_ROW_SELECT];
  const largeToggle = toggle('750 tasks', (value) => { large = value; project = null; applyProject(); });
  const restore = button('Restore', async () => {
    project = await persistence.load();
    large = false;
    largeToggle.input.checked = false;
    applyProject();
    status.textContent = 'Restored the saved project, including tasks and project collections.';
  });
  restore.disabled = true;
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.hidden = true;
  fileInput.accept = '.json,application/json';
  fileInput.setAttribute('aria-label', 'Import JSON file');
  fileInput.addEventListener('change', async () => {
    const file = fileInput.files?.[0];
    if (file) await run(async () => {
      project = await persistence.importFile(file);
      large = false;
      largeToggle.input.checked = false;
      applyProject();
      status.textContent = `Imported ${project.tasks.length} tasks from ${file.name}.`;
    });
    fileInput.value = '';
  });
  const options = document.createElement('div');
  options.className = 'gantt-production-options';
  const actions = document.createElement('div');
  actions.className = 'gantt-production-actions';
  options.append(
    toggle('Read only', (value) => { readOnly = value; applyConfig(); }).element,
    toggle('Español dates', (value) => { localized = value; applyConfig(); }).element,
    largeToggle.element,
  );
  actions.append(
    fileInput,
    button('Import JSON', () => fileInput.click()),
    button('JSON', () => persistence.exportFile(grid)),
    button('Save', async () => {
      await persistence.save(grid);
      restore.disabled = false;
      status.textContent = 'Saved the current edited project in browser memory.';
    }),
    restore,
    button('Print', () => persistence.print(grid)),
  );
  controls.append(options, actions);
  root.append(controls, status, toolbar, grid);
  parent.appendChild(root);
  applyProject();
  defineGanttToolbar(toolbar, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } });
  return () => { toolbar.textContent = ''; grid.remove(); root.remove(); };
}
Vuevue
<template>
  <section class="gantt-production-demo">
    <div class="gantt-production-controls">
      <div class="gantt-production-options">
        <label><input v-model="readOnly" type="checkbox" />Read only</label>
        <label><input v-model="localized" type="checkbox" />Español dates</label>
        <label><input v-model="large" type="checkbox" @change="project = null" />750 tasks</label>
      </div>
      <div class="gantt-production-actions">
        <input ref="importInput" type="file" hidden accept=".json,application/json" aria-label="Import JSON file" @change="importJson" />
        <button type="button" class="rv-btn" @click="importInput?.click()">Import JSON</button>
        <button type="button" class="rv-btn" @click="run(() => persistence.exportFile(getGrid()))">JSON</button>
        <button type="button" class="rv-btn" @click="save">Save</button>
        <button type="button" class="rv-btn" :disabled="!hasSaved" @click="restore">Restore</button>
        <button type="button" class="rv-btn" @click="run(() => persistence.print(getGrid()))">Print</button>
      </div>
    </div>
    <p class="gantt-production-status" role="status">{{ status }}</p>
    <div ref="toolbarRef" class="gantt-production-toolbar"></div>
    <RevoGrid ref="gridRef" class="gantt-production-grid" hide-attribution :theme="isDark ? 'darkCompact' : 'compact'"
      :plugins="plugins" :row-select.prop="rowSelect" :source="tasks" :columns="columns" :gantt.prop="ganttConfig"
      :gantt-dependencies.prop="dependencies" :gantt-calendars.prop="calendars" :gantt-resources.prop="resources"
      :gantt-assignments.prop="assignments" :gantt-baselines.prop="baselines" />
  </section>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';
import { SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES, SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR } from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';
const { isDark } = currentThemeVue();
const plugins = [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin];
const rowSelect = { rowOrder: true };
const columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
const readOnly = ref(false);
const localized = ref(false);
const large = ref(false);
const project = ref<ProjectSnapshot | null>(null);
const status = ref('Save and restore use browser memory only; no server is contacted.');
const hasSaved = ref(false);
const persistence = createProductionPersistence();
const tasks = computed(() => project.value ? productionSourceRows(project.value) : createProductionTasks(large.value));
const dependencies = computed(() => project.value?.dependencies ?? (large.value ? [] : SHOWCASE_DEPENDENCIES));
const calendars = computed(() => project.value?.calendars ?? [STANDARD_CALENDAR]);
const resources = computed(() => project.value?.resources ?? SHOWCASE_RESOURCES);
const assignments = computed(() => project.value?.assignments ?? (large.value ? [] : SHOWCASE_ASSIGNMENTS));
const baselines = computed(() => project.value?.baselines ?? (large.value ? [] : SHOWCASE_BASELINES));
const ganttConfig = computed(() => createProductionGanttConfig(readOnly.value, localized.value, project.value));
const gridRef = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);
const importInput = ref<HTMLInputElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);
let frame = 0;
function getGrid(): HTMLRevoGridElement {
  const value = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
  const grid = value && '$el' in value ? value.$el : value;
  if (!(grid instanceof HTMLElement)) throw new Error('The grid is still loading.');
  return grid as HTMLRevoGridElement;
}
async function run(action: () => unknown | Promise<unknown>) {
  try { await action(); } catch (error) { status.value = productionError(error); }
}
async function save() { await run(async () => { await persistence.save(getGrid()); hasSaved.value = true; status.value = 'Saved the current edited project in browser memory.'; }); }
async function restore() { await run(async () => { project.value = await persistence.load(); large.value = false; status.value = 'Restored the saved project, including tasks and project collections.'; }); }
async function importJson(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file) await run(async () => { project.value = await persistence.importFile(file); large.value = false; status.value = `Imported ${project.value.tasks.length} tasks from ${file.name}.`; });
  input.value = '';
}
function mountToolbar() {
  let grid: HTMLRevoGridElement;
  try { grid = getGrid(); } catch { frame = requestAnimationFrame(mountToolbar); return; }
  if (toolbarRef.value) defineGanttToolbar(toolbarRef.value, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } });
}
onMounted(async () => { await nextTick(); mountToolbar(); });
onBeforeUnmount(() => { cancelAnimationFrame(frame); if (toolbarRef.value) toolbarRef.value.textContent = ''; });
</script>
Reacttsx
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES,
  SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';

export default function GanttProductionPatterns() {
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const importInput = useRef<HTMLInputElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);
  const [readOnly, setReadOnly] = useState(false);
  const [localized, setLocalized] = useState(false);
  const [large, setLarge] = useState(false);
  const [project, setProject] = useState<ProjectSnapshot | null>(null);
  const [status, setStatus] = useState('Save and restore use browser memory only; no server is contacted.');
  const [hasSaved, setHasSaved] = useState(false);
  const persistence = useMemo(() => createProductionPersistence(), []);
  const plugins = useMemo(() => [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin], []);
  const rowSelect = useMemo(() => ({ rowOrder: true }), []);
  const columns = useMemo(() => SHOWCASE_COLUMNS_WITH_ROW_SELECT, []);
  const tasks = useMemo(() => project ? productionSourceRows(project) : createProductionTasks(large), [project, large]);
  const gantt = useMemo(() => createProductionGanttConfig(readOnly, localized, project), [readOnly, localized, project]);
  const dependencies = useMemo(() => project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES), [project, large]);
  const calendars = useMemo(() => project?.calendars ?? [STANDARD_CALENDAR], [project]);
  const resources = useMemo(() => project?.resources ?? SHOWCASE_RESOURCES, [project]);
  const assignments = useMemo(() => project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS), [project, large]);
  const baselines = useMemo(() => project?.baselines ?? (large ? [] : SHOWCASE_BASELINES), [project, large]);
  function getGrid(): HTMLRevoGridElement {
    if (!gridRef.current) throw new Error('The grid is still loading.');
    return gridRef.current;
  }
  async function run(action: () => unknown | Promise<unknown>) {
    try { await action(); } catch (error) { setStatus(productionError(error)); }
  }
  async function save() {
    await run(async () => {
      await persistence.save(getGrid());
      setHasSaved(true);
      setStatus('Saved the current edited project in browser memory.');
    });
  }
  async function restore() {
    await run(async () => {
      setProject(await persistence.load());
      setLarge(false);
      setStatus('Restored the saved project, including tasks and project collections.');
    });
  }
  async function importJson(event: React.ChangeEvent<HTMLInputElement>) {
    const input = event.currentTarget;
    const file = input.files?.[0];
    if (file) await run(async () => {
      const imported = await persistence.importFile(file);
      setProject(imported);
      setLarge(false);
      setStatus(`Imported ${imported.tasks.length} tasks from ${file.name}.`);
    });
    input.value = '';
  }
  useEffect(() => {
    const toolbar = toolbarRef.current;
    if (!toolbar || !gridRef.current) return;
    defineGanttToolbar(toolbar, { grid: gridRef.current, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } });
    return () => { toolbar.textContent = ''; };
  }, []);
  return <section className="gantt-production-demo">
    <div className="gantt-production-controls">
      <div className="gantt-production-options">
        <label><input type="checkbox" checked={readOnly} onChange={(event) => setReadOnly(event.currentTarget.checked)} />Read only</label>
        <label><input type="checkbox" checked={localized} onChange={(event) => setLocalized(event.currentTarget.checked)} />Español dates</label>
        <label><input type="checkbox" checked={large} onChange={(event) => { setLarge(event.currentTarget.checked); setProject(null); }} />750 tasks</label>
      </div>
      <div className="gantt-production-actions">
        <input ref={importInput} type="file" hidden accept=".json,application/json" aria-label="Import JSON file" onChange={importJson} />
        <button type="button" className="rv-btn" onClick={() => importInput.current?.click()}>Import JSON</button>
        <button type="button" className="rv-btn" onClick={() => { void run(() => persistence.exportFile(getGrid())); }}>JSON</button>
        <button type="button" className="rv-btn" onClick={save}>Save</button>
        <button type="button" className="rv-btn" disabled={!hasSaved} onClick={restore}>Restore</button>
        <button type="button" className="rv-btn" onClick={() => { void run(() => persistence.print(getGrid())); }}>Print</button>
      </div>
    </div>
    <p className="gantt-production-status" role="status">{status}</p>
    <div ref={toolbarRef} className="gantt-production-toolbar" />
    <RevoGrid ref={gridRef} className="gantt-production-grid" theme={currentTheme().isDark() ? 'darkCompact' : 'compact'} hideAttribution
      plugins={plugins} rowSelect={rowSelect} source={tasks} columns={columns} gantt={gantt}
      ganttDependencies={dependencies} ganttCalendars={calendars} ganttResources={resources}
      ganttAssignments={assignments} ganttBaselines={baselines} />
  </section>;
}
Angularts
import { ChangeDetectorRef, inject, AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { SHOWCASE_ASSIGNMENTS, SHOWCASE_BASELINES, SHOWCASE_COLUMNS_WITH_ROW_SELECT, SHOWCASE_DEPENDENCIES, SHOWCASE_RESOURCES, SHOWCASE_TOOLBAR_COLUMNS, STANDARD_CALENDAR } from './shared/gantt-project-data';
import { createProductionGanttConfig, createProductionTasks, createProductionPersistence, productionError, productionSourceRows } from './GanttProductionPatternsShared';

@Component({
  selector: 'gantt-production-patterns-grid', standalone: true, schemas: [NO_ERRORS_SCHEMA], imports: [RevoGrid],
  styleUrls: ['./gantt-production-patterns.css'], encapsulation: ViewEncapsulation.None,
  template: `<section class="gantt-production-demo">
    <div class="gantt-production-controls">
      <div class="gantt-production-options">
        <label><input type="checkbox" [checked]="readOnly" (change)="setReadOnly($any($event.target).checked)" />Read only</label>
        <label><input type="checkbox" [checked]="localized" (change)="setLocalized($any($event.target).checked)" />Español dates</label>
        <label><input type="checkbox" [checked]="large" (change)="setLarge($any($event.target).checked)" />750 tasks</label>
      </div>
      <div class="gantt-production-actions">
        <input #importFile type="file" hidden accept=".json,application/json" aria-label="Import JSON file" (change)="importJson($event)" />
        <button type="button" class="rv-btn" (click)="importFile.click()">Import JSON</button>
        <button type="button" class="rv-btn" (click)="exportJson()">JSON</button>
        <button type="button" class="rv-btn" (click)="save()">Save</button>
        <button type="button" class="rv-btn" [disabled]="!hasSaved" (click)="restore()">Restore</button>
        <button type="button" class="rv-btn" (click)="print()">Print</button>
      </div>
    </div>
    <p class="gantt-production-status" role="status">{{ status }}</p>
    <div #toolbar class="gantt-production-toolbar"></div>
    <revo-grid #grid class="gantt-production-grid" [theme]="theme" [hideAttribution]="true" [plugins]="plugins"
      [rowSelect]="rowSelect" [source]="tasks" [columns]="columns" [gantt]="ganttConfig" [ganttDependencies]="dependencies"
      [ganttCalendars]="calendars" [ganttResources]="resources" [ganttAssignments]="assignments" [ganttBaselines]="baselines"></revo-grid>
  </section>`,
})
export class GanttProductionPatternsGridComponent implements AfterViewInit, OnDestroy {
  private readonly changeDetector = inject(ChangeDetectorRef);
  @ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement>;
  @ViewChild('toolbar', { read: ElementRef }) toolbarRef!: ElementRef<HTMLElement>;
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  plugins = [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin]; rowSelect = { rowOrder: true }; columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
  calendars = [STANDARD_CALENDAR]; resources = [...SHOWCASE_RESOURCES]; assignments = [...SHOWCASE_ASSIGNMENTS]; baselines = [...SHOWCASE_BASELINES];
  readOnly = false; localized = false; large = false; tasks = createProductionTasks(); dependencies = SHOWCASE_DEPENDENCIES;
  project: ProjectSnapshot | null = null;
  status = 'Save and restore use browser memory only; no server is contacted.';
  hasSaved = false;
  private persistence = createProductionPersistence();
  ganttConfig = createProductionGanttConfig();
  private refresh() { this.ganttConfig = createProductionGanttConfig(this.readOnly, this.localized, this.project); }
  private refreshProject() {
    this.tasks = this.project ? productionSourceRows(this.project) : createProductionTasks(this.large);
    this.dependencies = [...(this.project?.dependencies ?? (this.large ? [] : SHOWCASE_DEPENDENCIES))];
    this.calendars = [...(this.project?.calendars ?? [STANDARD_CALENDAR])];
    this.resources = [...(this.project?.resources ?? SHOWCASE_RESOURCES)];
    this.assignments = [...(this.project?.assignments ?? (this.large ? [] : SHOWCASE_ASSIGNMENTS))];
    this.baselines = [...(this.project?.baselines ?? (this.large ? [] : SHOWCASE_BASELINES))];
    this.refresh();
  }
  private getGrid(): HTMLRevoGridElement {
    const grid = this.gridRef?.nativeElement;
    if (!grid) throw new Error('The grid is still loading.');
    return grid;
  }
  private async run(action: () => unknown | Promise<unknown>) {
    try { await action(); } catch (error) { this.status = productionError(error); }
    finally { this.changeDetector.markForCheck(); }
  }
  setReadOnly(value: boolean) { this.readOnly = value; this.refresh(); }
  setLocalized(value: boolean) { this.localized = value; this.refresh(); }
  setLarge(value: boolean) { this.large = value; this.project = null; this.refreshProject(); }
  exportJson() { return this.run(() => this.persistence.exportFile(this.getGrid())); }
  print() { return this.run(() => this.persistence.print(this.getGrid())); }
  save() {
    return this.run(async () => {
      await this.persistence.save(this.getGrid()); this.hasSaved = true;
      this.status = 'Saved the current edited project in browser memory.';
    });
  }
  restore() {
    return this.run(async () => {
      this.project = await this.persistence.load(); this.large = false; this.refreshProject();
      this.status = 'Restored the saved project, including tasks and project collections.';
    });
  }
  async importJson(event: Event) {
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];
    if (file) await this.run(async () => {
      this.project = await this.persistence.importFile(file); this.large = false; this.refreshProject();
      this.status = `Imported ${this.project.tasks.length} tasks from ${file.name}.`;
    });
    input.value = '';
  }
  ngAfterViewInit() { defineGanttToolbar(this.toolbarRef.nativeElement, { grid: this.gridRef.nativeElement, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } }); }
  ngOnDestroy() { if (this.toolbarRef?.nativeElement) this.toolbarRef.nativeElement.textContent = ''; }
}
Sharedts
import {
  exportProjectSnapshot,
  parseProjectSnapshotJson,
  type GanttPluginConfig,
  type GanttTaskSourceRow,
  type ProjectSnapshot,
} from '@revolist/gantt';
type ISODateString = import('@revolist/gantt').TaskEntity['startDate'];
import { getGanttScheduleSnapshot } from './recipes/current-schedule-snapshot';
import { ProjectSnapshotRestAdapter } from './recipes/rest-adapter';
import { printGanttContainer } from './recipes/print-pdf-recipe';
import {
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  STANDARD_CALENDAR,
} from './shared/gantt-project-data';

export function createProductionGanttConfig(readOnly = false, localized = false, project?: ProjectSnapshot | null): GanttPluginConfig {
  return {
    ...SHOWCASE_GANTT_CONFIG,
    ...(project ? { id: project.id, name: project.name, version: project.version, currency: project.currency, timeZone: project.timeZone, primaryCalendarId: project.primaryCalendarId, updatedAt: project.updatedAt, durationSettings: project.durationSettings, calendars: project.calendars } : {}),
    allowTaskCreate: true,
    taskCreateRow: true,
    readOnly,
    labels: localized ? {
      taskMode: { auto: 'Automática', manual: 'Manual' },
      workflowStatus: { 'not-started': 'Sin iniciar', 'in-progress': 'En curso', done: 'Finalizada', blocked: 'Bloqueada' },
    } : undefined,
    dateFormats: localized ? {
      locale: 'es-ES',
      table: { dateStyle: 'medium' },
      tooltip: { dateStyle: 'long' },
    } : undefined,
    visuals: {
      ...SHOWCASE_GANTT_CONFIG.visuals,
      showBaseline: true,
      showTaskLabels: true,
      timeRanges: [{ id: 'release-window', startDate: '2026-02-16', endDate: '2026-02-20', label: 'Release window', color: '#8b5cf633' }],
      milestoneLines: [{ id: 'go-live', date: '2026-02-27', label: 'Go live', color: '#e11d48' }],
      taskMarkerHook: (task) => task.deadlineDate ? [{ id: `deadline-${task.id}`, date: task.deadlineDate, label: 'Deadline' }] : [],
      taskBarColorHook: ({ row }) => row.id === SHOWCASE_TASKS[1].id ? { barColor: '#64748b', className: 'gantt-production-locked' } : undefined,
      taskTooltipFields: ['startDate', 'endDate', 'duration', 'percentDone', 'assignees'],
    },
  } as GanttPluginConfig;
}

export function createProductionTasks(large = false): GanttTaskSourceRow[] {
  if (!large) {
    return SHOWCASE_TASKS.map((task, index) => ({ ...task, ...(index === 1 ? { locked: true } : {}) })) as GanttTaskSourceRow[];
  }

  return Array.from({ length: 750 }, (_, index) => {
    const day = 2 + (index % 20);
    return {
      id: `large-${index + 1}`,
      name: `Virtualized task ${index + 1}`,
      startDate: `2026-02-${String(day).padStart(2, '0')}` as ISODateString,
      duration: 1 + (index % 5),
      durationUnit: 'day',
      percentDone: (index * 7) % 101,
      taskMode: 'manual',
      locked: index % 17 === 0,
    } satisfies GanttTaskSourceRow;
  });
}

export function productionSourceRows(project: ProjectSnapshot): GanttTaskSourceRow[] {
  return project.tasks.map((task) => ({
    ...task,
    durationUnit: task.durationUnit ?? 'hour',
    percentDone: task.progressPercent,
    taskMode: task.manuallyScheduled ? 'manual' : 'auto',
  }));
}

export async function getProductionProject(grid: HTMLRevoGridElement): Promise<ProjectSnapshot> {
  return getGanttScheduleSnapshot(grid);
}

export function downloadProductionSnapshot(project: ProjectSnapshot): void {
  const href = URL.createObjectURL(new Blob([exportProjectSnapshot(project, { space: 2 })], { type: 'application/json' }));
  const link = document.createElement('a');
  link.href = href;
  link.download = 'gantt-project.json';
  link.click();
  setTimeout(() => URL.revokeObjectURL(href), 0);
}

/** A real REST recipe with an injected browser-memory transport; no server is contacted. */
export function createProductionPersistence() {
  let savedJson: string | undefined;
  const adapter = new ProjectSnapshotRestAdapter({
    url: '/example/project',
    fetchImpl: (async (_input, init) => {
      if (init?.method === 'PUT') {
        savedJson = String(init.body);
        return new Response(null, { status: 204 });
      }
      return new Response(savedJson ?? 'No saved project', { status: savedJson ? 200 : 404 });
    }) as typeof fetch,
  });
  return {
    save: async (grid: HTMLRevoGridElement) => adapter.save(await getProductionProject(grid)),
    load: () => adapter.load(),
    importFile: async (file: Pick<File, 'text'>): Promise<ProjectSnapshot> => {
      const parsed = parseProjectSnapshotJson(await file.text());
      if (parsed.ok === false) throw new Error(parsed.error);
      return parsed.project;
    },
    exportFile: async (grid: HTMLRevoGridElement) => downloadProductionSnapshot(await getProductionProject(grid)),
    print: (grid: HTMLRevoGridElement) => {
      const result = printGanttContainer(grid.parentElement, { cleanupDelayMs: 60_000, hideSelectors: ['.gantt-production-controls', '.gantt-production-status', '.gantt-production-toolbar', '.gantt-toolbar', '.rgContextMenu'], metadata: { title: grid.gantt?.name ?? 'Project schedule' } });
      if (!result.ok) throw new Error('The print preview could not be opened.');
    },
  };
}

export function productionError(error: unknown): string {
  return error instanceof Error ? error.message : 'The operation failed.';
}
  1. Select Standard in the calendar list. Use New calendar for a separate schedule or Duplicate calendar to start from an existing one. Based on selects a base calendar for inheritance; Calendar name and Time zone edit its metadata.
  2. In Working week, choose Custom hours for a weekday to edit its start and finish times. Use Add working hours for split shifts, such as 09:00–12:00 and 13:00–17:00, or select Non-working to close the day. For a derived calendar, Use inherited working week controls whether it inherits its base week; individual weekdays can still override it.
  3. In Exceptions, click Add exception, give it a name, and set Start date and Finish date. Choose Non-working for a holiday or shutdown, or Custom hours for a shortened or extra working day. Repeat supports daily, weekly, monthly, and yearly rules, with Repeat interval and an optional Repeat until date.
  4. Navigate the month preview and select a date to inspect its effective working hours. The legend distinguishes non-working dates, local overrides, inherited exceptions, and the selected exception. Preview changes apply to the draft immediately.
  5. Click Save changes to validate and apply the calendars and recalculate the schedule. Cancel, the close button, or Escape discards the draft. Enable Read only above the demo, then reopen View → Working calendars to inspect the same dialog with editing disabled.

The demo enables calendar-aware scheduling. To see a holiday affect task dates, add a non-working exception on a working date within a task’s schedule, then save. Changes stay in the browser; the example does not write to a server. Creating a calendar does not assign it to tasks or resources automatically: set their calendarId to use it.

duration is stored in hours and can be displayed or entered as minutes, hours, days, weeks, months, quarters, or years. Unit conversion uses durationSettings and the effective calendar; see Duration Units and Conversion. Enable calendar exclusion when weekends, holidays, and closed hours should be skipped:

grid.gantt = {
scheduling: {
excludeHolidaysFromDuration: true,
},
};

Calendars use ISO weekday numbers where Monday is 1 and Sunday is 7:

grid.ganttCalendars = [{
id: 'cal-standard',
name: 'Standard',
timeZone: 'UTC',
workingDays: [1, 2, 3, 4, 5],
holidays: ['2026-05-25'],
hoursPerDay: 8,
}];

Each task references a calendar through calendarId. Resource capacity checks use the resource calendar when a resource is assigned.

CalendarEntity.hoursPerDay is the default conversion for task durations on that calendar. Project durationSettings.hoursPerDay can override it globally. Conversion settings do not change workingDays, holidays, or workingHours automatically.

Use baseCalendarId to reuse a working week. A child inherits the base week by default; inheritWorkingWeek: false keeps its local week instead. weekdayWorkingHours overrides individual ISO weekdays. An empty interval list closes that weekday.

grid.ganttCalendars = [
{
id: 'standard', name: 'Standard', timeZone: 'UTC',
workingDays: [1, 2, 3, 4, 5], holidays: [], hoursPerDay: 8,
workingHours: [{ start: '09:00', end: '17:00' }],
},
{
id: 'team', name: 'Team', timeZone: 'UTC',
baseCalendarId: 'standard',
workingDays: [], holidays: [], hoursPerDay: 8,
weekdayWorkingHours: {
1: [{ start: '10:00', end: '12:00' }, { start: '13:00', end: '16:00' }],
},
exceptions: [
{ id: 'shutdown', name: 'Shutdown', startDate: '2026-08-10', endDate: '2026-08-14', workingHours: [] },
{ id: 'annual-holiday', name: 'Annual holiday', startDate: '2026-12-25', workingHours: [], recurrence: { frequency: 'yearly' } },
{ id: 'short-day', name: 'Short day', startDate: '2026-12-24', workingHours: [{ start: '09:00', end: '12:00' }] },
],
},
];

Assign calendarId: 'team' to the relevant tasks or resources. Dated exceptions can close a date or supply replacement working intervals. Recurrence supports daily, weekly, monthly, and yearly rules with an optional positive interval and inclusive until date. A matching child exception takes precedence over its base calendar’s exception. Calendar dates use civil YYYY-MM-DD values; working intervals belong to that calendar date.

Keep scheduling.excludeHolidaysFromDuration: true when task scheduling should skip closed dates and intervals. hoursPerDay remains the conversion factor for canonical task durations; it does not imply every weekday has that many available hours. For example, seven hours starting Monday at 10:00 on a 10:00–12:00 / 13:00–16:00 Monday finish after consuming another two available hours on a subsequent working date.

Base calendars must exist and inheritance must be acyclic. Bounded nonworking exceptions, including a recurrence’s until date, must span at most 3,653 inclusive days from their start so they fit within the working-date traversal horizon. Invalid ranges, recurrence rules, and calendars without an effective working week are rejected before editor changes are committed.

Register the editor adapter alongside Gantt, then open it through its plugin instance:

import {
defineGanttToolbar,
GanttPlugin,
GanttCalendarEditorPlugin,
} from '@revolist/gantt';
grid.plugins = [GanttPlugin, GanttCalendarEditorPlugin];
// toolbarElement is an application-owned container above the grid.
defineGanttToolbar(toolbarElement, {
grid,
controls: { calendars: true },
});
// Open the same dialog programmatically after configuring the project calendars.
const editor = (await grid.getPlugins()).find(
plugin => plugin instanceof GanttCalendarEditorPlugin,
);
editor?.open({ selectedCalendarId: 'team', referenceDate: '2026-12-24' });

The dialog edits a draft. Cancel or Escape discards that draft. Save changes validates the complete calendar collection, commits it once, and recalculates the effective task schedule. Read-only projects allow inspection while disabling changes.

For the View → Working calendars toolbar entry, enable controls.calendars: true in defineGanttToolbar(...) and keep GanttCalendarEditorPlugin registered. The control is opt-in and has the accessible name Calendars.

Applications can also call gantt.setCalendars(nextCalendars) on their GanttPlugin instance. It returns false for invalid calendars, broken task/resource references, read-only projects, or a canceled gantt-before-calendar-change event. Accepted changes appear in gridedit with the gantt-calendar-change event tag and a gantt-calendar domain change, and participate in Gantt undo/redo. To review or reject a proposed change:

grid.addEventListener('gantt-before-calendar-change', event => {
if (!canEditProjectCalendars()) event.preventDefault();
});

The editor’s locale, localeText, and timeZones options customize its presentation. These settings do not convert existing task timestamps to another zone. Calendar editing supports reusable working weeks and exceptions; it does not imply Microsoft Project parity for every task/resource calendar combination.

Set persistent defaults on grid.ganttCalendarEditor (or grid.additionalData.ganttCalendarEditor). Pass per-open overrides to editor.open(...) or in the detail of gantt-calendar-edit. Controls, slots, and translation dictionaries merge by key; other options replace their configured value. Defaults are read on the next open, without discarding an open draft.

grid.ganttCalendarEditor = {
locale: 'es-ES',
controls: {
preview: false,
timeZone: false,
deleteCalendar: false,
repeatUntil: false,
},
localeText: {
title: 'Calendarios laborales',
save: 'Guardar cambios',
cancel: 'Cancelar',
calendar: { previousMonth: 'Mes anterior', nextMonth: 'Mes siguiente' },
validation: { 'invalid-calendar-name': 'Escriba un nombre para el calendario.' },
},
};
// Keeps the configured translations and other visibility settings.
editor?.open({ controls: { preview: true } });

locale controls localized dates and weekday/month names. localeText controls visible labels, accessible names, placeholders, calendar navigation, and validation text. Unspecified keys use English defaults. formatValidationIssue(issue, text) and formatSaveError(error, text) can supply application-specific localized messages. Error objects are not shown verbatim by default. Calendar names and exception names are application data. formatDate(value, locale) and formatTime(minutes, locale) override display formatting. Native date-picker widgets follow the browser locale; use the corresponding field replacement slot when an application needs a different date control.

Use dir for per-editor text direction, className for the editor root, and surfaceClass/overlayClass with existing CSS variables for dialog styling. draggable configures the shared dialog drag behavior. The same customization options work with Pro’s CalendarEditor, createCalendarEditor, and CalendarEditorDialogPlugin. Visibility is presentation configuration; hidden fields retain their values, and Gantt still validates the complete calendar collection on Save. Read-only project/grid/editor policies and pending saves also apply to custom controllers.

AreaVisibility keys
Dialogeditor, header, title, description, close, footer, cancel, save, validation
Calendar selectionsidebar, calendarList, calendarItem, calendarActions, newCalendar, duplicateCalendar, deleteCalendar
MetadatacalendarName, metadata, basedOn, timeZone
Working weektabs, workingWeek, weekday, inheritWeek, weekdayType, workingHours, hourRow, startTime, finishTime, addHours, removeHours
Exceptionsexceptions, exceptionList, exceptionItem, exceptionDetails, addException, deleteException, exceptionName, exceptionStartDate, exceptionFinishDate, exceptionType, exceptionWorkingHours, repeat, repeatInterval, repeatUntil
Previewpreview, legend, previewDetail

previewCalendar configures the existing Pro Calendar’s navigation, view controls, weekdays, outside days, Today action and date-label formatters. Its default remains the approved month preview; a per-open previewCalendar object replaces the configured object. Hiding the sidebar or preview lets the remaining content use the available space. Hiding a tab selects a remaining visible tab; hiding the tab bar keeps its current content accessible. If both tabs are hidden, their content is omitted.

Every visibility key is also a slot location: append .before, .replace, or .after. For example, calendarName.after adds custom metadata, exceptionDetails.before adds exception-specific content, and save.replace replaces the Save action. A hidden control suppresses its slots. A null replacement removes built-in content; omit the slot to retain the default.

Renderers use DOM containers so applications can mount Vanilla, Vue, React, or Angular content without depending on the editor’s internal rendering library:

import type { CalendarEditorDomRenderer } from '@revolist/gantt';
const calendarNote: CalendarEditorDomRenderer = {
mount(container, context) {
const note = container.ownerDocument.createElement('p');
const update = (next: typeof context) => {
note.textContent = next.selectedCalendar?.name ?? '';
};
container.append(note);
update(context);
return { update, destroy: () => note.remove() };
},
};
grid.ganttCalendarEditor = {
slots: { 'calendarName.after': calendarNote },
};

The renderer context contains detached draft calendars, the selected calendar/exception, localized text, errors, active tab, read-only and pending state. Repeated slots additionally receive the corresponding calendar, exception, weekday, or working-hour index. context.renderDefault(container) mounts the built-in content inside a replacement and keeps it updated, so a replacement can wrap an existing control.

Use context.controller to select calendars/exceptions/tabs, update the draft through updateCalendar(id, patch) or setCalendars(calendars), request save(), or close(). Mutating the context snapshot itself never commits data. Draft mutation returns false while read-only or pending; save() resolves to false when saving is blocked, invalid, or rejected. Renderers receive update on draft/state changes and destroy when replaced or closed. Dispose framework roots and event listeners there. Escape and the controller remain close paths when the built-in close button is hidden.

Use working-day lag when dependency offsets should skip weekends and holidays:

grid.gantt = {
scheduling: {
lagCalendar: 'working-days',
},
};

Example: A one-day lag after a Friday skips the weekend and a Monday holiday, landing on Tuesday.