Привет, меня зовут Женя, и я просто еще один из обитателей JavaScript вселенной, который хочет поделиться с вами интересным опытом в frontend-разработке, а именно как кастомизировать диаграмму Ганта.
Не так давно я присоединился к команде энтузиастов, которая работает над проектом по производству оборудования для ремонта автомобилей. Как и для любого проектного менеджмента, нам нужен был инструмент для планирования задач с следующим функционалом:
- оптимальное распределение ресурсов и нагрузки между сотрудниками
- мониторинг выполнения задач
- оценка эффективности и временных рамок проекта
Во многих современных приложениях для управления проектами для решения таких задач используются диаграммы Ганта. И я взялся за реализацию ее функциональности в нашем приложении.
Хотя можно было бы использовать готовый софт, в нашем случае нужно было сильно кастомизировать Гант под нужды проекта. Альтернатива разработки диаграммы с нуля — это слишком накладное и времязатратное мероприятие. Посовещавшись с коллегами, мы решили, что лучше всего найти готовый компонент среди библиотек JavaScript и настроить его под наш проект.
Основная проблема диаграмм Ганта заключается в том, что с большим количеством данных они становятся громоздкими и трудными для восприятия и управления. При поиске подходящей библиотеки мы выделили три основных критерия:
- простой для понимания интерфейс
- богатые возможности кастомизации
- ну и, само собой, хорошая производительность
Один из иностранных коллег уберег меня от долгих скитаний по просторам интернета в поисках необходимого виджета и подсказал JavaScript библиотеку DHTMLX Gantt. Ознакомившись с функциональными возможностями продукта, я решил, что стоит попробовать — библиотека удовлетворила наши требованиям по настройке диаграммы и оказалась простой в использовании.
Сама библиотека доступна в двух версиях — бесплатной и платной:
- в бесплатной нет некоторых фич и есть GPL v2 лицензия;
- а платная стоит от 700$ до 3000$ в зависимости от условий использования.
Для нашего проекта хватает фич бесплатной версии — ее мы и решили использовать.
Насколько я понял, загвоздка с GPL лицензиями и GPLv2 конкретно в том, что они требуют раскрывать исходный код (TLDRlegal). При этом не очень понятно, какой именно код веб приложения надо открывать — все приложение/только front-end/только код, который взаимодействует с библиотекой. Но так как наш проект мы делаем для внутренних нужд и не планируем никому распространять, как я понимаю, нас требование раскрытия кода не коснется. Если мы когда-нибудь решим продавать наше приложение другим предприятиям, тогда придется купить платную лицензию.
В этой статье я хочу поделиться с вами несколькими приемами работы с DHTMLX JavaScript Гантом. Я не стану расписывать все детали по настройке Ганта, но расскажу о наиболее интересных фишках, которые пригодились мне в работе.
Для проекта мне нужно было предоставить возможность пользователям обозначать задачи на диаграмме Ганта разными цветами прямо из интерфейса.
В DHTMLX Gantt контент можно редактировать двумя способами:
- вызвав форму редактирования (lightbox)
- воспользовавшись встроенными редакторами в области таблицы (inline editors), когда пользователь может редактировать данные с помощью горячих клавиш
Второй вариант редактирования задач как раз оказался тем, что мне нужно. Используя DHTMLX Gantt, я без труда смог реализовать свой тип редактора для выбора цвета — color picker, позволяющий присваивать цвет задачам проекта. Далее я подробно расскажу о реализации этой функциональности.
Инициализация
Пример я покажу на простом HTML5 и JavaScript, чтобы не требовалась сборка и было максимально понятно и без лишних сложностей.
Для начала нужно инициализировать Гант на странице. Для этого нужно подключить JS и CSS файлы библиотеки из коробки, а затем создать контейнер, в котором инициализировать Гант (подробная инструкция тут):
<!DOCTYPE html>
<html>
<head>
<script src="codebase/dhtmlxgantt.js"></script>
<link href="codebase/dhtmlxgantt.css" rel="stylesheet">
</head>
<body>
<div id="gantt_here" style='width:100vw; height:100vh;'></div>
<script>
window.addEventListener("DOMContentLoaded",() => {
gantt.init("gantt_here");
});
</script>
</body>
</html>
Прямо в HTML файле или же отдельно в js файле, после инициализации Ганта, нужно добавить данные, на основании которых Гант построит диаграмму. Для примера я добавлю тут 1 проект (Открытие производства оборудования) и 2 вложенные в него задачи (Определение рынка сбыта и Определение маркетинговой стратегии). Тут также задаются даты начала задач, их длительность, порядок выполнения и прогресс (степень выполнения задач):
gantt.parse({
data: [
{
id: 1, text: "Открытие производства оборудования", start_date: "01-05-2020", duration: 18, open: true
},
{
id: 2, text: "Определение рынка сбыта", start_date: "02-05-2020", duration: 4, parent: 1
},
{
id: 3, text: "Определение маркетинговой стратегии", start_date: "07-05-2020", duration: 5, parent: 1
}
],
links: [
{id: 1, source: 1, target: 2, type: "1"},
{id: 2, source: 2, target: 3, type: "0"}
]
});
});
Это самая простая часть.
Добавление Inline Editors
Опция встроенного редактирования позволяет вносить любые изменения прямо в таблице с помощью клавиатуры и мышки: создавать и обновлять задачи, устанавливать связи между ними, определять даты начала и окончания выполнения задачи или изменять общее отведенное на задачу время.
Колонку можно сделать редактируемой, добавив к ней свойство editor
:
gantt.config.columns = [
{name: "text", tree: true, width: '*', resize: true, editor: textEditor},
{name: "start_date", align: "center", resize: true, editor: dateEditor},
{name: "duration", align: "center", editor: durationEditor},
{name: "add", width: 44}
];
Объект редактора должен иметь свойство type
, которое соответствует нужному типу редактора, и свойство map_to
, которое определяет свойство объекта задачи, в которое редактор будет сохранять значения. Например, так настраивается редактор для полей с текстом, датами и длительностями задач:
const textEditor = {type: "text", map_to: "text"};
const dateEditor = {type: "date", map_to: "start_date", min: new Date(2020, 0, 1),
max: new Date(2021, 0, 1)};
const durationEditor = {type: "number", map_to: "duration", min:0, max: 100};
Остальные настройки относятся к определенным типам редакторов.
Создаем Свой Собственный Редактор
Чтобы дать возможность пользователям присваивать цвета задачам, нужно добавить в таблицу соответствующий столбец и прикрутить в него подходящий редактор.
Встроенного редактора для выбора цвета в библиотеке нет, поэтому вариантов у нас немного: использовать выпадающий список (select) или сделать новый редактор с селектором цвета (color picker). Я решил пойти по второму пути, то есть создать редактор-colorpicker.
Сначала я покажу на примере, как встроить простой HTML5 элемент формы input с типом color
, чтобы выбирать цвет задачи.
Для создания собственного редактора нужно добавить новый объект редактора в конфигурацию Ганта. Я сделал это по шаблону из документации:
gantt.config.editor_types.custom_editor = {
show: (id, column, config, placeholder) => {
// called when input is displayed, put html markup of the editor into placeholder
// and initialize your editor if needed:
placeholder.innerHTML `<div><input type='text' name='${column.name}'></div>`;
},
hide: () => {
// called when input is hidden
// destroy any complex editors or detach event listeners from here
},
set_value: (value, id, column, node) => {
// set input value
},
get_value: (id, column, node) => {
// return input value
},
is_changed: (value, id, column, node) => {
// called before save/close. Return true if new value differs from the original one
// returning true will trigger saving changes, returning false will skip saving
},
is_valid: (value, id, column, node) => {
// validate, changes will be discarded if the method returns false
return true/false;
},
save: (id, column, node) => {
// only for inputs with map_to:auto. complex save behavior goes here
},
focus: (node) => {
}
}
Чтобы создать свой редактор, я внес следующие изменения в код шаблона:
Первым делом я изменил название моего редактора и поменял функцию
show
, чтобы изменить тип вводимых данных на color:
gantt.config.editor_types.color = { show: (id, column, config, placeholder) => { placeholder.innerHTML = `<div><input type='color' name='${column.name}'></div>`; },
Метод
hide
мне не понадобился, поскольку элементу выбора цвета не требуется никаких деструкторов или постобработки после того, как он становится скрытым, поэтому я оставил его пустым:
hide: () => {},
Далее — методы
set_value
иget_value
:
set_value: (value, id, column, node) => { const input = node.querySelector("input"); input.value = value }, get_value: (id, column, node) => { const input = node.querySelector("input"); return input.value; },
Первый метод вызывается при открытии редактора, чтобы установить значение из объекта
task
. Второй метод вызывается, когда пользователь сохраняет редактор, а возвращаемое значение применяется к объекту задачи.
Следующая на очереди функция
is_changed
. Поскольку редакторы можно легко открывать и закрывать, я задал инициирование изменения данных только тогда, когда пользователь фактически меняет значение редактора:
is_changed: (value, id, column, node) => { const input = node.querySelector("input"); return input.value !== value; },
Внутри этого метода сравнивается исходное значение, определенное для редактора, с текущим значением и возвращается логическое значение true, если показатели различаются. Значение true обновит задачу новым значением, а false просто закроет редактор.
Принцип работы метода
is_valid
полностью соответствует своему названию и, возвращая false, сообщает Ганту, что введенное значение недопустимо и его необходимо сбросить:
is_valid: (value, id, column, node) => { const input = node.querySelector("input"); return !!input.value; },
Метод
save
мне не понадобился, так как он необходим для сложных редакторов, которые выполняют несколько изменений сразу, а не изменяют одно свойство задачи.
Использование метода
focus
помогло поместить фокус окна браузера в редактор:
focus: node => { const input = node.querySelector("input"); input.focus(); },
В итоге я получил свой собственный редактор для выбора цвета задачи в Ганте.
Добавление редактора в Гант
Далее мне нужно было добавить этот редактор в мой Гантт.
Для этого я добавил новый столбец к конфигу таблицы и привязал к нему конфиг редактора. Свойство type редактора цвета должно соответствовать значению для редактора, которое я использовал выше (в моем случае type: “color”).
const textEditor = {type: "text", map_to: "text"};
const dateEditor = {type: "date", map_to: "start_date", min: new Date(2020, 0, 1),
max: new Date(2021, 0, 1)};
const durationEditor = {type: "number", map_to: "duration", min:0, max: 100};
const colorEditor = {type: "color", map_to: "color"};
gantt.config.columns = [
{name: "text", tree: true, width: '*', resize: true, editor: textEditor},
{name: "start_date", align: "center", resize: true, editor: dateEditor},
{name: "duration", align: "center", editor: durationEditor},
{name: "color", align: "center", editor: colorEditor},
{name: "add", width: 44}
];
Я использовал свойство color
, поскольку Гант автоматически применит цвета из этого свойства. Теперь можно посмотреть, как все работает, если добавить к задачам значение цвета — например, color:"#FF0000":
{
id: 2, text: "Определение рынка сбыта", start_date: "02-05-2020", duration: 4, parent: 1, color:"#FF0000"
},
В качестве последнего штриха, я сделал так, чтобы цвета внутри столбца “color” отображались красиво. Для этого я использовал шаблон:
gantt.config.columns = [
{name: "text", tree: true, width: '*', resize: true, editor: textEditor},
{name: "start_date", align: "center", resize: true, editor: dateEditor},
{name: "duration", align: "center", editor: durationEditor},
{name: "color", align: "center", label:"Color", editor: colorEditor, template:
(task) => {
return `<div class='task-color-cell' style='background:${task.color}'></div>`
}
},
{name: "add", width: 44}
];
Я задал шаблон, который будет возвращать элемент контейнера div с указанным в стилях цветом фона. В файле css я добавил стили, чтобы красиво отображать цвет в контейнере:
.task-color-cell{
margin:10%;
width:20px;
height:20px;
border:1px solid #cecece;
display:inline-block;
border-radius:20px;
}
Реализованный пример с кодом можно посмотреть по ссылке: https://plnkr.co/edit/yGWtLzoELPrhJV2K?preview
Использование готового Color Picker виджета в редакторе
Поскольку в DHTMLX Gantt изменять цвета можно только через селект, то я решил использовать более гибкий способ для обозначения задач цветом, а именно интегрировать в Гант плагин jquery под названием Spectrum.
Первым шагом я добавил файлы библиотеки в Гант:
<!DOCTYPE html>
<html>
<head>
<script src="https://docs.dhtmlx.com/gantt/codebase/dhtmlxgantt.js"></script>
<link rel="stylesheet"href="https://docs.dhtmlx.com/gantt/codebase/dhtmlxgantt.css">
<script
src="http://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="http://bgrins.github.io/spectrum/spectrum.js"></script>
<link rel="stylesheet" href="http://bgrins.github.io/spectrum/spectrum.css">
<script src="script.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="gantt_here"></div>
</body>
</html>
После этого обновил контрол Ганта. Я задал переменную let editor, где будет храниться ссылка на наш редактор. Это необходимо, чтобы вызвать деструктор, когда элемент input будет скрыт.
Сначала я внес изменения в метод show
. Когда он вызывается, необходимо инициализировать и отобразить виджет селектора цвета.
На этом месте возникла неожиданная сложность: если вызывать editor.spectrum("show")
внутри метода show, редактор не появляется. Видимо, в момент вызова show
placeholder-элемент еще не добавлен в документ и у него нет размеров и позиции. В итоге я просто добавил минимальный тайм-аут, чтобы запускать color picker уже после того, как метод завершился и placeholder висит над нужным местом в таблице.
document.addEventListener("DOMContentLoaded", function(event) {
let editor;
gantt.config.editor_types.color = {
show: (id, column, config, placeholder) => {
placeholder.innerHTML = `<div><input type='color' name='${column.name}'></div>`;
editor = $(placeholder).find("input").spectrum({
change:() => {
gantt.ext.inlineEditors.save();
}
});
setTimeout(() => {
editor.spectrum("show");
})
}
Далее я определил метод “hide” — деструктор будет вызван, когда редактор будет закрыт:
hide: () => {
if(editor){
editor.spectrum("destroy");
editor = null;
}
Остальные методы не слишком отличаются от первоначальной реализации. Просто нужно изменить способ получения значений из контрола:
set_value: (value, id, column, node) => {
editor.spectrum("set", value);
},
get_value: (id, column, node) => {
return editor.spectrum("get").toHexString();
},
is_changed: function (value, id, column, node) {
const newValue = this.get_value(id, column, node);
return newValue !== value;
},
is_valid: function (value, id, column, node) {
const newValue = this.get_value(id, column, node);
return !!newValue;
},
focus:(node) => {
editor.spectrum("show");
}
После этого все должно работать как положено!
А это ссылка на мой пример со встроенным Color Picker.
Надеюсь, что эта статья поможет вам в настройке цвета задач в диаграмме Ганта.