Практические руководства

Информацию об основных принципах кастомизации читайте в соответствующей статье.

Как добавить столбцы в Дерево задач

Чтобы добавить столбец, создайте класс, наследуя его от gantt.views.tree. Внутри метода config разбейте исходный интерфейс и сконфигурируйте новый столбец:

class CustomTree extends gantt.views.tree {
  config() {
    const compact = this.getParam("compact", true);
    const ui = super.config();
 
    ui.columns.splice(compact ? 3 : 2, 0, {
      id: "progress",
      header: "~",
      template: "#progress#%", // поле "progress" приходит из данных
      width: 40,
    });
 
    if (!compact) ui.width += 40;
 
    return ui;
  }
}

Не забудьте переопределить класс по умолчанию с помощью свойства override:

webix.ui({
  view: "gantt",
  override: new Map([[gantt.views.tree, CustomTree]]),
});

Related sample:  Gantt: Как добавить столбцы в Дерево задач

Как добавить произвольный тултип

Чтобы добавить расширенный тултип для задач, создайте класс, наследуя его от класса по умолчанию gantt.views["chart/bars"]. ВВнутри него вызовите метод GetTooltip() и опишите темплейт для нового тултипа:

class CustomBarsView extends gantt.views["chart/bars"] {
    GetTooltip(obj) {
        if (obj.type == "milestone") return `You've reached ${obj.text}`;
                const type = obj.type.charAt(0).toUpperCase() + obj.type.slice(1);
                const days = obj.duration > 1 ? "days" : "day";
                const title = obj.text ? obj.text : "(no title)";
                return `${type}: ${title}<br>
                    Scheduled for ${obj.duration} ${days}<br>
                    ${obj.details ?? ""}`;
    }
}

Не забудьте переопределить класс по умолчанию с помощью свойства override:

webix.ui({
  view: "gantt",
  override: new Map([[gantt.views["chart/bars"], CustomBarsView]]),
});

Related sample:  Gantt: Как добавить произвольный тултип

Как масштабировать шкалы

Этот пример показывает, как можно реализовать масштабирование шкал и переключаться между шкалами разных временных интервалов. Это особенно удобно при больших объёмах задач и проектов.

Прежде всего опишите функцию, которая будет отвечать за смену шкал:

function resetScales(v, o) {
  const current = gantt1.getService("local").getScales();
  if (!(originalEndDate || originalStartDate)) {
    originalStartDate = webix.Date.add(current.start, 1, o, true);
    originalEndDate = webix.Date.add(current.end, -1, o, true);
  }
 
  const cellWidth = cellWidths[v];
  const scales = getScales(v);
 
  const start = webix.Date.add(originalStartDate, -1, v, true);
  const end = webix.Date.add(originalEndDate, 1, v, true);
 
  gantt1
    .getService("local")
    .setScales(
      start,
      end,
      !(v == "day"),
      cellWidth,
      current.cellHeight,
      scales
    );
}
 
// возвращает массив шкал, которые будут отрисованы
function getScales(minScale) {
  const scales = [];
  switch (minScale) {
    case "year":
      scales.push(yearScale);
      break;
    case "quarter":
      scales.push(yearScale, quarterScale);
      break;
    // другие условия
  }
  return scales;
}

Для непосредственного зуммирования, определите список Richselect с необходимыми опциями. При смене значения в контроле будет вызываться функция resetScales:

const zoom = {
  view: "richselect",
  label: "Zoom:",
  value: "month",
  width: 300,
  options: ["hour", "day", "week", "month", "quarter", "year"],
  on: {
    onChange: resetScales,
  },
};

Добавьте тулбар и поместите ричселект в его поле elements:

webix.ui({
  rows: [
    {
      view: "toolbar",
      id: "toolbar",
      css: "webix_dark",
      paddingX: 10,
      elements: [zoom, {}],
    },
    {
      view: "gantt",
      id: "gantt1",
      url: "https://docs.webix.com/gantt-backend/",
      // другие свойства
    },
  ],
});

Related sample:  Gantt: Как масштабировать шкалы

Всплывающее окно с информацией о событии и широкая форма для редактирования

Этот пример показывает, как убрать правую панель и:

  • отображать небольшой попап с информацией вместо правой панели
  • открывать форму для редактирования на месте сетки задач.

Изменения касаются только полноэкранного режима, компактный режим остаётся без изменений.

Подготавливаем попап

Компонент Task Info необходимо поместить в попап, поэтому внесём некоторые правки заранее:

  • укажите внутренние отступы и размеры UI элементов
  • спрячьте некоторые детали (напр., successors, predecessors...)
  • опишите действи при клике по кнопке "Edit", чтобы оно открывало форму на месте сетки задач.
class CustomInfo extends gantt.views["task/info"] {
   // стилизация
  config() {
    this.Compact = this.getParam("compact", true);
    const ui = super.config();
    if (!this.Compact) {
      ui.body.rows[0].padding = 4; //toolbar
      ui.body.rows[1].padding = 4; //main area
      ui.body.rows[1].rows[0].autoheight = true; //template
      ui.body.rows[1].rows[1].inputWidth = 0; //button
    }
    return ui;
  }
  // открываем форму для редактирования
  EditTask() {
    if (!this.Compact) {
      this.getParentView().Hide();
      this.app.show("top/task.form");
    } else super.EditTask();
  }
 // убираем некоторые детали
  InfoTemplate(obj) {
    obj.targets = obj.sources = [];
    delete obj.details;
    return super.InfoTemplate(obj);
  }
}

После этого создайте класс InfoPopup с компонентом Popup, который содержит вышеупомянутый компонент Info внутри. Также опишите методы, чтобы показывать/прятать попап:

class InfoPopup extends gantt.views.JetView {
  config() {
    return {
      view: "popup",  width: 350,
      body: CustomInfo,
    };
  }
  Show(node) {
    this.getRoot().show(node);
  }
  Hide() {
    this.getRoot().hide();
  }
}

Показываем и прячем Info попап

Следующим шагом будет показ/скрытия попапа при клике по событию. Для этого вам необходимо переопределить класс по умолчанию gantt.views["chart/bars"]:

class CustomBarsView extends gantt.views["chart/bars"] {
  config() {
    const ui = super.config();
    // показываем InfoPopup при клике по задаче
    ui.cells[1].on.onItemClick = (id, e, node) => {
      this.State.$batch({
        parent: null, selected: id,
      });
      this.Info.Show(node);
    };
    return ui;
  }
  init(view) {
    super.init(view);
    // создаём попап
    this.Info = this.ui(InfoPopup);
    // обрабатываем событие, чтобы спрятать попап
    this.on(this.app, "edit:stop", () => {
      this.State.selected = this.State.parent = null;
      this.Info.Hide();
    });
  }
}

Подготавливаем форму для редактирования

Теперь переопределим Form View. При необходимости вы можете разбить форму на столбцы, а поле "Notes" показывать справа. Так как форма перекрывает сетку задач, можно убрать редактирование при изменении данных.

class CustomForm extends scheduler.views["event/form"] {
    config() {
      let ui = super.config();
      if (!this.Compact) {
        // textarea
        const notes = ui.body.rows[1].elements.splice(5, 1)[0];
        notes.labelPosition = "top";
        notes.height = 334;
 
        // форма с 3 столбцами
        const form = ui.body.rows[1];
        form.cols = [
          { margin: form.margin, rows: ui.body.rows[1].elements },
          { rows: [notes, {}] },
          { width: 300 },
        ];
        form.margin = 20;
        delete ui.body.rows[1].elements;
        ui.body.rows[1] = form;
 
        // bar
        ui.body.rows[0].padding.right = 335;
      }
 
      return ui;
    }
}

Теперь необходимо поправить логику CustomForm:

  • при клике по кнопке "Done" данные должны сохраняться, а форма - закрываться
  • при клике по кнопке "Close", вы можете спрашивать у пользователей, сохранять ли измнения, если таковые есть.
// действие при клике по кнопке "Done" 
 Done(exit) {
  // сохраняем данные при создании нового события или редактировании уже существующего
    if (this.Form.isDirty())
      this.UpdateTask().then(() => this.Back(exit));
    else this.Back(exit);
 }
// действие при клике по иконке "Close" 
Close() {
    // спрашивает пользователя в случае изменённых данных
  if (this.Form.isDirty()) {
    webix.confirm("Save changes?")
   .then(() => this.Done(true))
               .catch(() => this.Back(true));
  } else this.Back(true);
}
// очищаем форму и показывает сетку задач
Back(exit) {
    if (!this.Compact) {
      this.Form.clear();
      this.State.selected = this.State.parent = null;
      this.app.show("top/chart");
    } else super.Back(close);
}

Настраиваем навигацию

Чтобы показывать форму вместо сетки задач, необходимо заменить статичный subview на динамичным и затем показывать задачи после инициализации.

class CustomTopView extends gantt.views.top {
  config() {
      const ui = super.config();
       // вставляем динамический subview для показа формы или сетки задач
       ui.cells[0].cols[2] = { $subview: true };
       return ui;
   }
   init(view) {
        super.init(view);
        // показывает сетку задач
        this.show("chart/bars");
    }
  }

После этого вы сможете показывать форму из CustomInfo:

// метод EditTask класса CustomInfo (описан выше)
this.app.show("top/task.form");

и открывать форму при создании новой задачи:

// открываем форму при создании новой задачи
ShowTask(path) {
     if (this.Compact) super.ShowTask(path);
    else if (path === "form")
        this.show("task.form");
}
// сохраняем исходную логику только для компактного режима
HideTask() {
       if (this.Compact);
         super.HideTask();
}

Не забудьте переопределить класс по умолчанию с помощью свойства override:

webix.ui({
  view: "gantt",
  override: new Map([
    [gantt.views["chart/bars"], CustomBarsView],
    [gantt.views.top, CustomTopView],
    [gantt.views["task/info"], CustomInfo],
    [gantt.views["task/form"], CustomForm]
  ])
});

Related sample:  Gantt: Всплывающее окно с информацией о событии и широкая форма для редактирования

Изменение времени задачи

В этом примере вы можете увидеть как:

  • задать возможность установки времени задачи и отображения его в Дереве задач и на Информационной панели
  • переопределить значение тултипа, чтобы показывать время задачи, а также отображать ее продолжительность в часах.

Настройка шкалы

Прежде всего, включите шкалу с часами в массив scales в конфигурации Gantt.

view:"gantt",
scales: [
  { unit: "day", format: "%M %d" },
  { unit: "hour", format: "%H:00" },
]

Далее вам необходимо модифицировать несколько внутренних классов виджета, чтобы отобразить время задач в интерфейсе.

Изменение формы и Информационной панели

Для того, чтобы предоставить возможность устанавливать время задачи, создайте класс, наследуя его от gantt.views["task/form"]. Измените JSON конфигурацию формы, чтобы выключить таймпикер для внутренних календарей:

class CustomForm extends gantt.views["task/form"] {
  config() {
    const ui = super.config();
 
    const form = ui.body.rows[1];
    const startCal = form.elements[2];
    startCal.timepicker = startCal.suggest.body.timepicker = true;
    const endCal = form.elements[3];
    endCal.timepicker = endCal.suggest.body.timepicker = true;
 
    return ui;
  }
}

Для того, чтобы отобразить установленное время на Информационной панели, создайте класс, наследуя его от gantt.views["task/info"]. Верните строку, которая содержит дату и время, используя метод DateFormat:

class CustomInfo extends gantt.views["task/info"] {
  DateFormat(date) {
    return webix.i18n.fullDateFormatStr(date);
  }
}

Отображение времени в Дереве задач

Для того, чтобы показать дату и время в “Дереве задач”, создайте класс, наследуя его от gantt.views.tree. Для отображения строки с датой и временем, измените JSON конфигурацию, возвращенную методом config родительского класса:

class CustomTree extends gantt.views.tree {
  config() {
    const tree = super.config();
 
    tree.columns[1].format = webix.i18n.fullDateFormatStr;
    tree.columns[1].minWidth = 170;
 
    return tree;
  }
  //еще один метод
}

Расширение тултипа задачи

Вы можете изменить тултип задачи таким образом, что дата задачи и его продолжительность будут учитывать значение времени. Создайте класс, наследуя его от gantt.views["chart/bars"]. Верните шаблон с информацией о времени задачи из метода GetTooltip:

class CustomBars extends gantt.views["chart/bars"] {
  GetTooltip(obj, _) {
    const parser = webix.i18n.fullDateFormatStr;
    let tip = `${obj.text || _("(no title)")}<br>
<br>${_("Start date")}: ${parser(obj.start_date)}`;
 
    if (obj.type != "milestone") {
      const duration =
            obj.duration < 1
      ? this.GetPreciseDuration(obj, _)
      : this.GetDayDuration(obj, _);
 
      tip += `<br>${_("End date")}: ${parser(obj.end_date)}
<br>${_("Lasts")} ${duration}`;
    }
    return tip;
  }
 
  /*получает продолжительность задач, 
  которые длятся менее 1 дня, и возвращает ее в часах 
  (для почасовой шкалы и шкал у которых задано свойство *precise:true*).  
  В ином случае будет возвращен 1 день*/
 
  GetPreciseDuration(obj, _) {
    const { minUnit, precise } = this.Local.getScales();
    const showHours = minUnit === "hour" || precise;
    return showHours 
      ? `${Math.round(obj.duration * 24)} ${_("hours")}`
      : `1 ${_("day")}`;
  }
 
  GetDayDuration(obj, _) {
    return `${obj.duration} ${obj.duration > 1 ? _("days") : _("day")}`;
  }
}

Не забудьте переопределить классы по умолчанию с помощью свойства override:

webix.ui({
  view: "gantt",
  url: "https://docs.webix.com/gantt-backend/",
  override: new Map([
    [gantt.views.tree, CustomTree],
    [gantt.views["task/info"], CustomInfo],
    [gantt.views["task/form"], CustomForm],
    [gantt.views["chart/bars"], CustomBars],
  ]),
});

Related sample:  Gantt: Изменение времени задачи

Создание снэпшота текущего положения задач

В данном примере показано, как установить планируемые даты в виде снэпшота текущего положения задач.

Подготовка

Чтобы включить планируемые даты, задайте baseline значение true:

webix.ui({
    view: "gantt",
    baseline: true,
    ...
})

Нам нужен какой-то элемент управления для создания снэпшота. Сделаем кнопку с названием "Create a snapshot". При нажатии будет вызываться пользовательская функция createSnapshot ():

{
    view: "button",
    value: "Create a snapshot",
  ...
    click: () => createSnapshot(),
},

Получение текущего состояния

Используя метод serialize(), мы получаем текущее состояние Gantt:

function createSnapshot() {
    const localTasks = $$("gantt")
        .$app.getService("local")
        .tasks();
    const data = localTasks.serialize();
...

Установка свойств базового плана

Для сохранения дат начала и окончания как planned_start и planned_end, а продолжительности как planned_duration, мы объявляем отдельную функцию setBaseline():

function setBaseline(branch) {
    branch.forEach(t => {
        if (t.type != "milestone") {
            t.planned_start = t.start_date;
            t.planned_end = t.end_date;
            t.planned_duration = t.duration;
            if (t.data) setBaseline(t.data);
        }
    });
}

Затем мы вызовем ее внутри метода createSnapshot() для установки базового плана для всех задач:

    ...
    setBaseline(data);
    ...

Обновление локальных данных

В конце нам необходимо очистить все коллекции методом clearAll() и с помощью метода parse() загрузить в компонент наши измененные данные:

    ...
    localTasks.clearAll();
    localTasks.parse(data);
}

Related sample:  Gantt: Creating Snapshots

Наверх