DHTMLX Gantt Live Edit

Hi All,

I create this dhx gantt live edit plugin, in case some one need it, or can be an idea for dhtmlx core team to improve feature set.

I am not so understand about licensing, as dhtmlx use dual license, and one of them is GPL, I think that this doesn’t violate license term, in case it violating license, I am happy to delete this.

ok, so to enable live edit plugin, just include the file (attached)

 <script src="lib/gantt/ext/dhtmlxgantt_live_edit.js"></script> 

you can now double click the grid part, and edit the task data on-the-go instead of using pop-up task-editor window. To bring up task editor window, double click the bar on the gantt-chart.

There is still lot room for improvement, so maybe dhx team can implement it better and be included in future release.

dhtmlxgantt_live_edit.js

Gantt.plugin(function(gantt){
    gantt.config.live_edit = true;
    gantt._live_edit = {
        input : null,
        activeCell : null,
        activeTask : null,
        activeTaskId : null,
        activeColumnId: null,
        activeColumnIndex: null,
        isDateField : false,
        calendar : null
    }

    if (!gantt.config.live_edit) {
        return;
    }

    gantt._init_grid = function () {
        this._click.gantt_close = this.bind(function (e, id, trg) {
            this.close(id);
            return false;
        }, this);
        this._click.gantt_open = this.bind(function (e, id, trg) {
            this.open(id);
            return false;
        }, this);


        this._click.gantt_row = this.bind(function (e, id, trg) {
            if (e.target.getAttribute('id') == 'gantt_live_editor') {
                return;
            }

            if (id !== null) {
                var task = this.getTask(id);
                if(this.config.scroll_on_click)
                    this.showDate(task.start_date);
                this.callEvent("onTaskRowClick", [id, trg]);
            }
        }, this);

        this._click.gantt_grid_head_cell = this.bind(function (e, id, trg) {
            var column = trg.getAttribute("column_id");

            if (!this.callEvent("onGridHeaderClick", [column, e]))
                return;

            if (column == "add") {
                this._click.gantt_add(e, this.config.root_id);
                return;
            }

            if (this.config.sort) {
                var sorting_method = column,
                    conf;

                for(var i = 0; i < this.config.columns.length; i++){
                    if(this.config.columns[i].name == column){
                        conf = this.config.columns[i];
                        break;
                    }
                }

                if(conf && conf.sort !== undefined && conf.sort !== true){
                    sorting_method = conf.sort;

                    if(!sorting_method){ // column sort property 'false', no sorting
                        return;
                    }
                }

                var sort = (this._sort && this._sort.direction && this._sort.name == column) ? this._sort.direction : "desc";
                // invert sort direction
                sort = (sort == "desc") ? "asc" : "desc";
                this._sort = {
                    name: column,
                    direction: sort
                };
                this.sort(sorting_method, sort == "desc");
            }
        }, this);

        if (!this.config.sort && this.config.order_branch) {
            this._init_dnd();
        }

        this._click.gantt_add = this.bind(function (e, id, trg) {
            if (this.config.readonly) return;

            var taskId = gantt.addTask({
                id: (new Date()).valueOf(),
                text:"New Task",
                start_date:new Date(),
                duration:1
            }, id ? id : this.config.root_id);

            this.liveEdit(taskId);
            return false;
        }, this);

        if(this._init_resize){
            this._init_resize();
        }

    };

    function getNodeIndex(node) {
        var index = 0;
        while ( (node = node.previousElementSibling) ) {
            index++;
        }
        return index;
    }

    function getCellNode(node) {
        if (node.className.indexOf("gantt_cell") > -1) {
            return node;
        }

        while (node.className.indexOf("gantt_cell") < 0){
            node = node.parentNode;
        }

        return node;
    }

    function getRowNode(cellNode) {
        if (cellNode.className.indexOf("gantt_row") > -1) {
            return cellNode;
        }

        while (cellNode.className.indexOf("gantt_row") < 0){
            cellNode = cellNode.parentNode;
        }

        return cellNode;
    }

    function getCellContent(node) {
        var content = null;
        for (var i = 0; i < node.childNodes.length; i++) {
            if (node.childNodes[i].className == "gantt_tree_content") {
                content = node.childNodes[i];
                break;
            }
        }

        return content;
    }

    function getCellIndex(cellNode) {
        var index = 0;

        while (cellNode = cellNode.previousSibling) {
            index++;
        }
        return index;
    }

    function getColumnId(cellNode) {
        var row = getRowNode(cellNode);
        var index = getCellIndex(cellNode);

        return gantt.$grid_scale.childNodes[index].getAttribute('column_id');
    }

    function updateTask() {
        if (gantt._live_edit.isDateField) {
            gantt._live_edit.activeTask[gantt._live_edit.activeColumnId] = gantt._live_edit.calendar.getDate();
            gantt._live_edit.calendar.detachObj(gantt._live_edit.input);
            gantt._live_edit.input.removeAttribute('data-calendar');
        } else {
            gantt._live_edit.activeTask[gantt._live_edit.activeColumnId] = gantt._live_edit.input.value;
        }

        gantt.updateTask(gantt._live_edit.activeTaskId);

    }

    /** prepare live editor */
    gantt._live_edit.input      = document.createElement('input');
    gantt._live_edit.input.id   = 'gantt_live_editor';
    gantt._live_edit.input.style.width   = '100%';

    if (typeof(dhtmlXCalendarObject) == 'function') {
        gantt._live_edit.calendar = new dhtmlXCalendarObject(gantt._live_edit.input);
    }

    /** move editor to next cell on tab, or close editor on focus */
    gantt.event(gantt._live_edit.input, 'keydown', function(e){
        var tabCode = 9;
        var enterCode = 13;

        if (e.keyCode != tabCode && e.keyCode != enterCode) {
            return;
        }

        if (e.keyCode == enterCode){
            updateTask();
            return;
        }

        if(e.preventDefault) {
            e.preventDefault();
        }

        updateTask();

        /** search for next cell */
        var currentRow  = gantt.getTaskRowNode(gantt._live_edit.activeTaskId);
        var currentCell = currentRow.childNodes[gantt._live_edit.activeColumnIndex];
        var nextCell    = currentCell.nextElementSibling;

        /** move to next row if cell not found */
        if (nextCell == null || (nextCell.className.indexOf('gantt_last_cell') >= 0 && getCellContent(nextCell) == null)) {
            var nextRow = getRowNode(currentCell).nextElementSibling;

            if (nextRow == null) {
                gantt._live_edit.calendar.hide();
                return false;
            }

            nextCell = nextRow.childNodes[0];

            gantt.showTask(gantt.getNext(gantt._live_edit.activeTaskId));
            gantt.updateTask(gantt._live_edit.activeTaskId);
        } else if (getCellContent(nextCell) == null) {
            nextCell = nextCell.nextElementSibling;
        }

        var event = new MouseEvent('dblclick', {
            'view': window,
            'bubbles': true,
            'cancelable': true
        });
        nextCell.dispatchEvent(event);

        return false;
    });


    /** take live editor into action */
    gantt.attachEvent("onGanttReady", function(){
        gantt.eventRemove(gantt.$container, 'dblclick', gantt._on_dblclick);
        gantt.event(gantt.$task_data, 'dblclick', gantt._on_dblclick);

        gantt.event(gantt.$grid_data, 'dblclick', function(e){
            var target = e.target || e.srcElement;

            if (e.target.getAttribute('id') == 'gantt_live_editor') {
                return;
            }

            gantt._live_edit.activeTaskId   = gantt.locate(e);
            gantt._live_edit.activeTask     = gantt.getTask(gantt._live_edit.activeTaskId);
            gantt._live_edit.activeCell     = getCellNode(target);
            gantt._live_edit.activeColumnId = getColumnId(gantt._live_edit.activeCell);
            gantt._live_edit.isDateField    = gantt._live_edit.activeColumnId.indexOf('date') >= 0;
            gantt._live_edit.activeColumnIndex = getNodeIndex(gantt._live_edit.activeCell);


            var row             = getRowNode(gantt._live_edit.activeCell);
            var content         = getCellContent(gantt._live_edit.activeCell);
            var contentCache    = content.innerText;
            var styleCache      = getComputedStyle(content, null);
            var cellIndex       = 0;
            content.innerHTML   = '';

            gantt._live_edit.input.style.height  = styleCache.height;
            gantt._live_edit.input.value         = contentCache;

            content.appendChild(gantt._live_edit.input);

            if (gantt._live_edit.isDateField) {
                gantt._live_edit.input.setAttribute('data-calendar', 'true');
                gantt._live_edit.input.setAttribute('data-old-value', contentCache);
                gantt._live_edit.calendar.setFormatedDate(contentCache, "%Y-%m-%d");
                gantt._live_edit.calendar.attachObj(gantt._live_edit.input);
                gantt._live_edit.calendar.show();
            } else {
                gantt._live_edit.calendar.hide();
                gantt._live_edit.calendar.detachObj(gantt._live_edit.input);
            }
            document.querySelectorAll('#gantt_live_editor')[0].focus();
            document.querySelectorAll('#gantt_live_editor')[0].select();
        });
    });

    gantt.liveEdit = function(taskId, columnId = null) {
        gantt.showTask(taskId);

        var rowNode     = gantt.getTaskRowNode(taskId);
        var cellNode    = rowNode ? rowNode.childNodes[0] : null;
        var dblClick    = new MouseEvent('dblclick', {
            'view': window,
            'bubbles': true,
            'cancelable': true
        });

        if (!cellNode) {
            return;
        }

        if (columnId == null) {
            cellNode.dispatchEvent(dblClick);
        } else {
            while (getColumnId(cellNode) != columnId) {
                cellNode = cellNode.nextElementSibling;

                if (cellNode == null) {
                    break;
                }
            }

            if (cellNode) {
                cellNode.dispatchEvent(dblClick);
            }
        }

        document.querySelectorAll('#gantt_live_editor')[0].focus();
        document.querySelectorAll('#gantt_live_editor')[0].select();
    }
});

regards,
Ikhsan

Hi,
that’s very cool! Thanks a lot!

I’ve modified the code a bit so it would work both with enterprise and gpl builds (see HTML tab for the code)
snippet.dhtmlx.com/94fea1ce6

It is actually because my client want to mimic live edit functionality of bryntum’s gantt bryntum.com/examples/gantt- … vanced/#en , but they already purchase dhx gantt license.

since it is dhx gantt competitor, it seems to be good idea to add live edit function into next release of dhx gantt :wink:

Hi everybody!

This live edit is the funcionality I need in my application.
I was able to load this code provided, but there are a couple things I didn’t understand about it. Could you guys help me out?

  • When adding a new task the lightbox is showing up, do I need to override some method?

  • I have added some new columns in the grid, some of them are dates, how can I make this live edit recognize it is a date?

  • I am planning to let the user do whatever he wants to do in the component before saving the tasks. He will create new tasks, edit them, erase some, but it will be saved in the database only after clicking on the “save” button. Are the data stored in the “tasks” property?

I am also able to achieve this, But how do I force it to call Update Task method after i click enter or click somewhere on gantt to save edited text/title/start_date/duration etc on inline eidting?

amanbindal,
That already should work because it is included in the updateTask() function:

[code]if (e.keyCode == enterCode){
updateTask();
return;
}

function updateTask() {
if (gantt._live_edit.isDateField) {
gantt._live_edit.activeTask[gantt._live_edit.activeColumnId] = gantt._live_edit.calendar.getDate();
gantt._live_edit.calendar.detachObj(gantt._live_edit.input);
gantt._live_edit.input.removeAttribute(‘data-calendar’);
} else {
gantt._live_edit.activeTask[gantt._live_edit.activeColumnId] = gantt._live_edit.input.value;
}

gantt.updateTask(gantt._live_edit.activeTaskId);

}[/code]

I checked it with Ruby on Rails + SQL database and seems that it works correctly.


arthurmmedeiros,
In case if you still need the solution and for anyone else:

You can cancel the lightbox from showing up by using onBeforeLightbox event handler. If you return “false”, the lightbox won’t show up:

gantt.attachEvent("onBeforeLightbox", function(id) { return false; });

Here is a link to the article about onBeforeLightbox event:
docs.dhtmlx.com/gantt/api__gant … event.html

You can manually convert the data user inputs, here is an article for the reference:
docs.dhtmlx.com/gantt/api__gant … other.html
But probably you don’t need it. Because the code sample already has that option. You just need to have “date” in the column name.
Or If you want to change “date” to something else edit this string:

gantt._live_edit.isDateField    = gantt._live_edit.activeColumnId.indexOf('date') >= 0;
  1. The task data is stored in its properties. There are several ways how you can get and modify the task’s data, here is an example how you can find the task #7 and modify its text:
gantt.getTask(7).text = "New text";

You can find more information in this article:
docs.dhtmlx.com/gantt/api__gantt_gettask.html

Here is the updated example, now there is gantt 5.1 with working lightbox (it doesn’t show) and datepicker:
snippet.dhtmlx.com/4bfbe0ef4