dhtmlxGantt предлагает два способа редактирования содержимого:
Редактирование "на месте" позволяет вносить изменения прямо из грида: создавать и обновлять задачи, устанавливать связи между ними, задавать даты начала и окончания, а также изменять длительность — всё это через встроенные редакторы.
Чтобы включить редактирование "на месте", необходимо:
var textEditor = {type: "text", map_to: "text"};
var dateEditor = {type: "date", map_to: "start_date", min: new Date(2018, 0, 1),
max: new Date(2019, 0, 1)};
var durationEditor = {type: "number", map_to: "duration", min:0, max: 100};
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}
];
Related sample: Inline editing
Подробнее об API объекта inlineEditors читайте в статье Inline Editors Extension.
Также доступно видео, демонстрирующее реализацию редактирования "на месте" в гриде.
Встроенные редакторы определяются в объекте конфигурации editor_types.
В Gantt предопределено несколько встроенных редакторов:
{ type: "duration", map_to: "duration", formatter: formatter }
Этот тип редактора полезен, когда требуется указывать длительность с числом и единицей измерения длительности, например: 5 days
. По умолчанию используется Duration Formatter. Вы также можете настроить его или использовать пользовательский форматтер.
var editors = {
text: {type: "text", map_to: "text"},
start_date: {type: "date", map_to: "start_date", min: new Date(2018, 0, 1),
max: new Date(2019, 0, 1)},
end_date: {type: "date", map_to: "end_date", min: new Date(2018, 0, 1),
max: new Date(2019, 0, 1)},
duration: {type: "number", map_to: "duration", min:0, max: 100},
priority: {type:"select", map_to:"priority", options:gantt.serverList("priority")},
predecessors: {type: "predecessor", map_to: "auto"}
};
Начиная с версии 6.3, для встроенных редакторов типа date нет предустановленных минимальных и максимальных значений.
Если вы хотите, чтобы видимые даты на временной шкале определяли min и max значения для редактора date (если не заданы пользовательские min/max), можно использовать динамические функции min/max:
const dateEditor = {type: "date", map_to: "start_date",
min: function(taskId){
return gantt.getState().min_date
},
max: function( taskId ){
return gantt.getState().max_date
}
};
Если вы используете включительный формат даты окончания для задач и хотите корректно поддерживать редактирование "на месте" в гриде, потребуется специальный редактор для включительных дат окончания:
// редактор для включительных дат окончания
// используем стандартный редактор, но переопределяем методы set_value/get_value
var dateEditor = gantt.config.editor_types.date;
gantt.config.editor_types.end_date = gantt.mixin({
set_value: function(value, id, column, node){
var correctedValue = gantt.date.add(value, -1, "day");
return dateEditor.set_value.apply(this, [correctedValue, id, column, node]);
},
get_value: function(id, column, node) {
var selectedValue = dateEditor.get_value.apply(this, [id, column, node]);
return gantt.date.add(selectedValue, 1, "day");
},
}, dateEditor);
var textEditor = {type: "text", map_to: "text"};
var startDateEditor = {type: "date", map_to: "start_date"};
var endDateEditor = {type: "end_date", map_to: "end_date"};
var durationEditor = {type: "number", map_to: "duration", min:0, max: 100};
gantt.config.columns = [
{name: "text", label: "Name", tree: true, width: 200, editor: textEditor,
resize: true},
{name: "duration", label: "Duration", width:80, align: "center",
editor: durationEditor, resize: true},
{name: "start_date", label: "Start", width:140, align: "center",
editor: startDateEditor, resize: true},
{name: "end_date", label: "Finish", width:140, align: "center",
editor: endDateEditor, resize: true}
];
// обновляем шаблоны lightbox и грида для отображения включительных дат окончания
gantt.templates.task_end_date = function(date){
return gantt.templates.task_date(new Date(date.valueOf() - 1));
};
var gridDateToStr = gantt.date.date_to_str("%Y-%m-%d");
gantt.templates.grid_date_format = function(date, column){
if(column === "end_date"){
return gridDateToStr(new Date(date.valueOf() - 1));
}else{
return gridDateToStr(date);
}
}
Related sample: Редактор включительной даты окончания
Подробнее о форматировании дат окончания читайте в статье Отображение даты окончания задачи и включительные даты окончания.
Эта функция доступна только в PRO-версии.
Начиная с версии 6.3, Gantt поддерживает указание типов связей, а также значений лаг/лид непосредственно во встроенном редакторе.
Для этого используйте модуль Link Formatter и передайте экземпляр LinksFormatter в редактор predecessor:
var formatter = gantt.ext.formatters.durationFormatter({
enter: "day",
store: "day",
format: "auto"
});
var linksFormatter = gantt.ext.formatters.linkFormatter({durationFormatter: formatter});
var editors = {
text: {type: "text", map_to: "text"},
start_date: {type: "date", map_to: "start_date",
min: new Date(2018, 0, 1), max: new Date(2019, 0, 1)},
end_date: {type: "date", map_to: "end_date",
min: new Date(2018, 0, 1), max: new Date(2019, 0, 1)},
duration: {type: "duration", map_to: "duration",
min:0, max: 100, formatter: formatter},
priority: {type: "select", map_to: "priority",
options:gantt.serverList("priority")},
predecessors: {type: "predecessor", map_to: "auto", formatter: linksFormatter} };
gantt.config.columns = [
{name: "wbs", label: "#", width: 60, align: "center", template: gantt.getWBSCode},
{name: "text", label: "Name", tree: true, width: 200, editor: editors.text,
resize: true},
{name: "start_date", label: "Start", width:80, align: "center",
editor: editors.start_date, resize: true},
{name: "predecessors", label: "Predecessors",width:80, align: "left",
editor: editors.predecessors, resize: true, template: function(task){
var links = task.$target;
var labels = [];
for(var i = 0; i < links.length; i++){
var link = gantt.getLink(links[i]);
labels.push(linksFormatter.format(link)); }
return labels.join(", ")
}},
{name:"add"}
];
Related sample: Inline editing - keyboard navigation mode
Ниже приведены примеры кода для пользовательских редакторов:
Вы можете создать собственный редактор "на месте", определив новый объект редактора следующим образом:
gantt.config.editor_types.custom_editor = {
show: function (id, column, config, placeholder) {
// вызывается при отображении редактора, вставьте HTML-разметку в placeholder
// и инициализируйте необходимые редакторы:
var html = "<div><input type='text' name='" + column.name + "'></div>";
placeholder.innerHTML = html;
},
hide: function () {
// вызывается при скрытии редактора
// здесь можно очистить сложные редакторы или удалить обработчики событий
},
set_value: function (value, id, column, node) {
// установить значение в input
},
get_value: function (id, column, node) {
// вернуть значение из input
},
is_changed: function (value, id, column, node) {
// вызывается перед сохранением/закрытием. Верните true, если новое значение отличается от исходного
// true инициирует сохранение изменений, false — пропускает сохранение
},
is_valid: function (value, id, column, node) {
// валидация значения, возврат false отменяет изменения
return true/false;
},
save: function (id, column, node) {
// для input с map_to:auto. Здесь реализуется сложная логика сохранения
},
focus: function (node) {
}
}
Описание методов:
map_to:auto
. Параметры:
Ключевые моменты для повторно используемых редакторов:
get_value
должен только возвращать текущее значение редактора, не модифицируя объект задачи. Если значение валидно, Gantt обновит задачу автоматически.map_to
для указания, какое свойство задачи обновляет редактор, чтобы не привязывать это внутри редактора и обеспечить повторное использование.hide
часто можно оставить пустым, если не требуется удалять обработчики событий или уничтожать сложные виджеты.is_changed
и is_valid
:
is_changed
должен возвращать true только если значение действительно изменилось, чтобы избежать лишних обновлений.is_valid
предотвращает ввод некорректных данных.save
и установите map_to
в "auto". В этом случае gantt не будет напрямую изменять задачу, а вызовет save
для применения изменений.Пример простого редактора для числового значения. Метод hide
пустой, а save
не реализован.
var getInput = function(node){
return node.querySelector("input");
};
gantt.config.editor_types.simpleNumber = {
show: function (id, column, config, placeholder) {
var min = config.min || 0,
max = config.max || 100;
var html = "<div><input type='number' min='" + min +
"' max='" + max + "' name='" + column.name + "'></div>";
placeholder.innerHTML = html;
},
hide: function () {
// пусто, так как очистка не требуется после удаления редактора
},
set_value: function (value, id, column, node) {
getInput(node).value = value;
},
get_value: function (id, column, node) {
return getInput(node).value || 0;
},
is_changed: function (value, id, column, node) {
var currentValue = this.get_value(id, column, node);
return Number(value) !== Number(currentValue);
},
is_valid: function (value, id, column, node) {
return !isNaN(parseInt(value, 10));
},
focus: function (node) {
var input = getInput(node);
if (!input) {
return;
}
if (input.focus) {
input.focus();
}
if (input.select) {
input.select();
}
}
};
Далее используйте его как встроенный редактор:
var numberEditor = {type: "simpleNumber", map_to: "quantity", min:0, max: 50};
gantt.config.columns = [
...
{name: "quantity", label: "Quantity", width: 80, editor: numberEditor,
resize: true},
...
];
Поскольку Gantt автоматически удаляет DOM-элемент редактора, дополнительная очистка в hide
не требуется.
При использовании более сложных виджетов в редакторах "на месте" может понадобиться реализация метода hide
для очистки.
Например, вот реализация поля DatePicker с использованием jQuery UI. Виджет datepicker необходимо уничтожить, когда редактор удаляется из DOM.
Необходимые зависимости:
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
Редактор:
gantt.config.editor_types.custom_datepicker_editor = {
show: function (id, column, config, placeholder) {
placeholder.innerHTML = "<div><input type='text' id='datepicker' name='" +
column.name + "'></div>";
$("#datepicker").datepicker({
dateFormat: "yy-mm-dd",
onSelect: function(dateStr){
gantt.ext.inlineEditors.save()
}
});
},
hide: function (node) {
$("#datepicker").datepicker( "destroy" );
},
set_value: function (value, id, column, node) {
$("#datepicker").datepicker("setDate", value);
},
get_value: function (id, column, node) {
return $("#datepicker").datepicker( "getDate" );
},
is_changed: function (value, id, column, node) {
return (+$("#datepicker").datepicker( "getDate" ) !== +value);
},
is_valid: function (value, id, column, node) {
return !(isNaN(+$("#datepicker").datepicker( "getDate" )))
},
save: function (id, column, node) {
},
focus: function (node) {
}
};
let dateEditor = {
type: "custom_datepicker_editor",
map_to: "start_date"
};
gantt.config.columns = [
{name: "text", tree: true, width: '*', resize: true},
{name: "start_date", align: "center", resize: true, editor: dateEditor},
{name: "duration", align: "center"},
{name: "add", width: 44}
];
Related sample: Использование jQuery Datepicker в редакторе
Функция save
полезна, когда редактору требуется одновременно обновить несколько свойств задачи или изменить объекты, отличные от задач.
В таких случаях вы всё равно можете реализовать get_value
для встроенной валидации, однако Gantt не будет пытаться напрямую применить значение редактора к задаче. Вместо этого будет вызвана функция save
.
После вызова save
вы должны обработать входные значения и применить необходимые изменения к Gantt с помощью собственного кода. После завершения метода save
Gantt инициирует событие onSave. Однако при этом не вызывается gantt.updateTask для обновлённой строки.
Важно! Метод save
вызывается только если в конфигурации редактора установлено map_to:"auto"
:
var editors = {
...
predecessors: {type: "predecessor", map_to: "auto"}
};
Хорошим примером является встроенный редактор предшественников. Упрощённую реализацию можно посмотреть в соответствующем примере:
Related sample: Built-in predecessor editor
В этом режиме используется мышь для фокусировки на ячейках и горячие клавиши для навигации между ними:
Related sample: Inline editing
В этом режиме клавиатура используется как для навигации, так и для редактирования ячеек грида с помощью определённых клавиш или их сочетаний:
Чтобы включить навигацию с клавиатуры для редактирования, необходимо:
gantt.plugins({
keyboard_navigation: true
});
gantt.config.keyboard_navigation = true;
gantt.config.keyboard_navigation_cells = true;
Также можно включить placeholder row — это пустая строка в конце списка задач. Пользователь может редактировать эту строку для добавления новых задач.
gantt.config.placeholder_task = true;
Если нужно, чтобы фокус автоматически переходил к placeholder-задаче сразу после добавления новой задачи, используйте:
gantt.config.placeholder_task = {
focusOnCreate: true
};
При необходимости можно также активировать автоматическое определение типа задачи:
gantt.config.auto_types = true;
Related sample: Inline editing - keyboard navigation mode
Можно определить собственные сочетания клавиш — указать, как открываются редакторы, а также обработать события редактора, такие как открытие, закрытие, начало и завершение редактирования. Для этого создайте отдельный объект с нужной логикой и передайте его в специальный метод для применения вашей схемы:
var mapping = {
init: function(inlineEditors){
// модуль inlineEditor инициализирован
// добавить глобальные слушатели начала/завершения редактирования
},
onShow: function(inlineEditors, node){
// редактор открыт
},
onHide: function(inlineEditors, node){
// редактор скрыт
// при необходимости очистить изменения из onShow
}
};
gantt.ext.inlineEditors.setMapping(mapping);
Related sample: Inline editing - Custom keyboard mapping
Представьте, что в вашем Gantt используются навигация с клавиатуры, инлайн-редакторы и placeholder-задача. Рассмотрим два распространённых сценария.
Сценарий 1. После ввода имени новой placeholder-задачи и нажатия Tab ожидается, что Gantt откроет следующую ячейку для этой задачи. Вместо этого фокус переходит к следующей placeholder-задаче ниже, и инлайн-редактор не открывается.
Сценарий 2. После ввода имени новой placeholder-задачи и клика по следующей ячейке Gantt переводит фокус к следующей placeholder-задаче, а не к выбранной ячейке.
Кастомное отображение позволяет обработать эти ситуации, указав, как инлайн-редактор должен реагировать на действия мышью и клавиатурой. Пример:
Related sample: Gantt. Custom mapping for placeholder task
При редактировании ячеек в гриде могут возникать ошибки.
Чтобы предотвратить сохранение некорректных значений, необходимо валидировать ввод перед закрытием редактора. Это можно сделать двумя способами:
Вот как валидация влияет на поведение редактора:
Если редактор в ячейке грида открыт с помощью мыши, применяются следующие правила:
Подробнее о клиентской или серверной валидации смотрите в статье Валидация.
Когда включена валидация, Gantt сбрасывает некорректные значения и закрывает редактор, требуя повторного открытия ячейки для исправления значения.
Практичным способом избежать этого может быть показ alert-окна, позволяющего пользователю исправить значение. Это реализуется с помощью кастомной схемы управления с клавиатуры, например:
function editAnotherCell(inlineEditors){
var value = inlineEditors.getValue();
if(confirm(`does '${value}' look ok to you?`)){
inlineEditors.save();
}
}
var mapping = {
init: function(inlineEditors){
gantt.attachEvent("onTaskClick", function (id, e) {
var cell = inlineEditors.locateCell(e.target);
if (cell && inlineEditors.getEditorConfig(cell.columnName)) {
if (inlineEditors.isVisible()) editAnotherCell(inlineEditors)
else inlineEditors.startEdit(cell.id, cell.columnName);
return false;
}
return true;
});
gantt.attachEvent("onEmptyClick", function () {
inlineEditors.hide();
return true;
});
},
onShow: function(inlineEditors, node){
node.onkeydown = function (e) {
e = e || window.event;
if(e.defaultPrevented){
return;
}
var keyboard = gantt.constants.KEY_CODES;
var shouldPrevent = true;
switch (e.keyCode) {
case gantt.keys.edit_save:
var value = inlineEditors.getValue();
if(confirm(`does '${value}' look ok to you?`)){
inlineEditors.save();
}
break;
case gantt.keys.edit_cancel:
inlineEditors.hide();
break;
case keyboard.TAB:
if(e.shiftKey){
if (inlineEditors.isVisible()) editAnotherCell(inlineEditors)
else inlineEditors.editPrevCell(true);
}else{
if (inlineEditors.isVisible()) editAnotherCell(inlineEditors)
else inlineEditors.editNextCell(true);
}
break;
default:
shouldPrevent = false;
break;
}
if(shouldPrevent){
e.preventDefault();
}
};
},
onHide: function(inlineEditors, node){}
};
gantt.ext.inlineEditors.setMapping(mapping);
gantt.init("gantt_here");
Related sample: Custom keyboard mapping
В режиме одиночного выбора клик по задаче сразу открывает инлайн-редактор.
В режиме множественного выбора клик по невыбранной задаче сначала выделяет её, а инлайн-редактор открывается только при втором клике. Чтобы редактор открывался при первом клике, включите настройку inline_editors_multiselect_open:
gantt.plugins({
multiselect: true
});
...
gantt.config.inline_editors_multiselect_open = true;
К началу