- PVSM.RU - https://www.pvsm.ru -

Прикручивание диаграммы Гантта

При разработке системы документооборота возникла необходимость отображать данные в виде диаграммы Гантта [1]. После непродолжительных поисков был найден подходящий бесплатный компонент, который нужно было прикрутить к «движку» easla.com [2].
Прикручивание диаграммы Гантта - 1
Мой опыт прикручивания JS-компоненты к движку на Yii с описание, кодом и примерами под катом.

Прежде всего скажу, что учитывая специфику разрабатываемой системы документооборота первой мыслью была разработка компоненты «с нуля» своими силами. Но, сведя все требования в длинный список, прикинув объем работ, количество кода на PHP и на JavaScript, немного остыл. И, как и многие другие [3] понял, что разумнее поискать готовые компоненты с необходимым функционалом, даже если они окажутся платными.

Требования к компоненте были следующие:

  • Отображение диаграммы Гантта в стиле близком easla.com [2]
  • Обратная связь посредством изменения задач мышкой
  • Множество event'ов
  • Скорость работы.

Учитывая, что диаграмма Гантта весьма популярный метод отображения информации, компонент в Интернете оказалось очень много. Просматривал и отбирал их наверное целый день. Конечно, сперва, отказывался от самих простых, представляющих исключительно базовый функционал, и постепенно формировал короткий список наиболее мощных и продвинутых компонент.
Внимательно изучил только парочку компонент, одной из которых и была DHTMLX Gantt [4]. На ней и остановился.

Задача

Требования к интеграции компоненты были такие:

  • Интеграция в easla.com в виде компоненты
  • Отображение данных постранично/целиком
  • Фильтр и сортировка средствами easla.com [2]
  • Отображение шапки диаграммы также, как в других видах (таблицах)
  • Обратная связь при изменении задач в диаграмме.

Первый же пункт вызвал ряд вопросов. Компонента на 100% клиентская, т.е. вся написана на JavaScript, а нужно, чтобы она инициализировалась с помощью PHP и принимала множество входных параметров. К счастью, DHTMLX Gantt [4] написана очень качественно и с помощью входных параметров ее можно настроить ровно так, как надо.

Постраничное отображение – следующая головная боль. На форуме разработчика звучали вопросы о «постраничности», но в ответ только недоумение типа: «Зачем это нужно? Это же нарушает идеологию диаграммы Гантта!» Однако в моем случае без «постраничности» никак, поэтому схема реализации тоже была найдена еще до реализации.

Фильтр и сортировка такой же непростой вопрос как и постраничное отображение. Сортировка в компоненте есть своя, но она может быть использована только при отображении всех данных в таблице сразу. Иначе говоря, при постраничном отображении встроенная сортировка работать не будет. Фильтр работает аналогично. Мне пришлось потратить пару дней на изучение реализации render'а в компоненте, чтобы понять, получится ли отрисовывать шапку по-своему. Примерно так:
Прикручивание диаграммы Гантта - 2

К счастью, с обратной связью особых проблем не увидел. В компоненте полно event'ов, а также присутствует dataProcessor, посредством которого можно обновлять данные. Кстати, с компонентой предлагают целую толпу классов для интеграции, но мне они не пригодились.

Реализация на PHP

Прежде всего, встал вопрос, какой класс унаследовать, чтобы создать свою компоненту. Очень хотелось унаследовать класс CGridView [5], но после пары попыток стало понятно, что она избыточна. Одновременно стало очевидно, что наследуемый класс должен обладать базовым набором методов для render'а и «пагинации». В конечном счете остановился на CBaseListView [6].
Создаваемый класс AlxdDhtmlxGantt очень хотелось сделать похожим на CGridView, чтобы не «изобретать велосипед» и не создавать себе трудностей, поэтому в новый класс начал копировать все нужные свойства прямо из CGridView вместе с комментариями. На текущий момент, в AlxdDhtmlxGantt из уникальных свойств добавлены следующие:

    public $onTaskSelected;
    public $onTaskOpened;
    public $onTaskClosed;
    public $onTaskDragStart;
    public $onTaskDrag;
    public $onBeforeTaskDrag;
    public $onBeforeTaskChanged;
    public $onAfterTaskDrag;
    public $itemsTag = 'div';
    public $dataProcessorUrl;
    public $itemsStyle='height:500px;';
    public $taskAttributes = array();
    public $scales;
    public $tree = false;

Как очевидно из названий, все on* — это обработчики событий, используемые, преимущественно, для дополнительного render'а checkbox'ов.

taskAttributes – это одно из важных свойств, которое должно содержать список атрибутов отображаемой модели, которые будут использованы диаграммой для наименования, даты начала и окончания задачи. Формат следующий:

public $taskAttributes = array(
	'text'=>'description',
	'start_date'=>'plan_start_date',
	'end_date'=>'plan_end_date'
);

Вместо end_date можно указать duration. Главное, что должен быть указан или атрибут окончания задачи, или ее продолжительности. Подробнее в документации [7].

scales – это еще одно важное свойство, которое должно описывать временную шкалу диаграммы Гантта. На мой взгляд, разработчики компоненты немного намудрили с настройками временной шкалы, выделив параметры основной шкалы в scale_unit [8] и date_scale [9], а параметры доп. шкалы в subscales [10]. Но, надеюсь, им там было виднее. Я объединил настройку шкалы в одно свойство класса, которое должно принимать массив всех временных шкал. Надо одну шкалу – значит в массиве будет только одна шкала. Надо две – значит две и т.д. Формат следующий:

Public $scales = array(
	array('unit'=>'year', 'step'=>1, 'date'=>'%Y')
	array('unit'=>'month', 'step'=>1, 'date'=>'%F, %Y')
);

По-моему так проще.

По аналогии с CGridView в AlxdDhtmlxGantt нужно инициализировать колонки. Их инициализация один в один как в CGridView. Не скрою, метод просто скопирован и немного поправлен.

initColumns

    protected function initColumns()
    {
        if($this->columns===array())
        {
            if($this->dataProvider instanceof CActiveDataProvider)
                $this->columns=$this->dataProvider->model->attributeNames();
            elseif($this->dataProvider instanceof IDataProvider)
            {
                // use the keys of the first row of data as the default columns
                $data=$this->dataProvider->getData();
                if(isset($data[0]) && is_array($data[0]))
                    $this->columns=array_keys($data[0]);
            }
        }
        $id=$this->getId();
        foreach($this->columns as $i=>$column)
        {
            if(is_string($column))
                $column=$this->createDataColumn($column);
            else
            {
                if(!isset($column['class'])) {
                    $column['class'] = 'CDataColumn';
                }
                $column=Yii::createComponent($column, $this);
            }
            if(!$column->visible)
            {
                unset($this->columns[$i]);
                continue;
            }
            if($column->id===null)
                $column->id=$id.'_c'.$i;
            $this->columns[$i]=$column;
        }

        $tree_initiated = false;
        foreach($this->columns as $column) {
            $column->init();

            if ($column instanceof CDataColumn && $this->tree && !$tree_initiated) {
                $this->tree_column_name = $column->name;
                $tree_initiated = true;
            }
        }
    }

Определившись с входными параметрами, пришло время решить вопрос с отображением данных. Взвесив все «за» и «против» пришел к выводу, что первоначальное заполнение диаграммы данными удобнее делать с помощью js, а обратную связь обеспечить через ajax. Перекрыл метод renderItems. Он теперь почти ничего не render'ит:

renderItems

    public function renderItems()
    {
        if($this->dataProvider->getItemCount()>0 || $this->showTableOnEmpty)
        {
            echo CHtml::openTag($this->itemsTag, array('class'=>$this->itemsCssClass, 'style'=>$this->itemsStyle));
            //render container only
            //content render in javascript
            echo CHtml::closeTag($this->itemsTag);
        }
        else
            $this->renderEmptyText();
    }

Формирование массива значений для компоненты осуществляется с помощью метода getData, который перебирает данные (все или для активной страницы) и передает их в виде массива.

    public function getData()
    {
        $ret = array('data'=>array());
        $data = $this->dataProvider->getData();
        $n = count($data);
        if($n > 0) {
            for($row=0; $row < $n; ++$row)
                $ret['data'][] = $this->getDataRow($row);
        }
        return $ret;
    }

Печальным оказался тот факт, что public метод renderDataCell [11] отрисовывает значение вместе с тэгом td. Пришлось использовать protected метод renderDataCellContent [12], вызывая его с помощью ReflectionMethod [13]. Примерно так:

$r = new ReflectionMethod($column, 'getDataCellContent');
$r->setAccessible(true);
$value = $r->invoke($column, $row, $data);
$ret[$column->name] = $value;

Полная инициализации компоненты для отображения осуществляется в методе registerClientScript. В нем же осуществляется загрузка всех необходимых скриптов и стилей отображения, включая скрипт локализации.

registerClientScript

    public function registerClientScript()
    {
        $id = $this->getId();

        if($this->ajaxUpdate===false)
            $ajaxUpdate=false;
        else
            $ajaxUpdate=array_unique(preg_split('/s*,s*/',$this->ajaxUpdate.','.$id,-1,PREG_SPLIT_NO_EMPTY));

        $itemsSelector = $this->itemsTag;
        $itemsCssClass = explode(' ',$this->itemsCssClass,2);
        if (is_array($itemsCssClass)) {
            $itemsSelector .= '.'.$itemsCssClass[0];
        }

        $options=array(
            'ajaxUpdate'=>$ajaxUpdate,
            'ajaxVar'=>$this->ajaxVar,
            'pagerClass'=>$this->pagerCssClass,
            'loadingClass'=>$this->loadingCssClass,
            'filterClass'=>$this->filterCssClass,
//            'tableClass'=>$this->itemsCssClass,
//            'selectableRows'=>$this->selectableRows,
            'enableHistory'=>$this->enableHistory,
            'updateSelector'=>$this->updateSelector,
            'filterSelector'=>$this->filterSelector,
            'itemsSelector'=>$itemsSelector,
        );
        if($this->ajaxUrl!==null)
            $options['url']=CHtml::normalizeUrl($this->ajaxUrl);
        if($this->ajaxType!==null)
            $options['ajaxType']=strtoupper($this->ajaxType);
        if($this->enablePagination)
            $options['pageVar']=$this->dataProvider->getPagination()->pageVar;
        foreach(array('beforeAjaxUpdate', 
                    'afterAjaxUpdate', 
                    'ajaxUpdateError', 
                    'onTaskSelected', 
                    'onTaskOpened',
                    'onTaskClosed', 
                    'onTaskDragStart', 
                    'onTaskDrag',
                    'onBeforeTaskDrag',
                    'onBeforeTaskChanged',
                    'onAfterTaskDrag',
                    /*, 'selectionChanged'*/) as $event)
        {
            if($this->$event!==null)
            {
                if($this->$event instanceof CJavaScriptExpression)
                    $options[$event]=$this->$event;
                else
                    $options[$event]=new CJavaScriptExpression($this->$event);
            }
        }

        $options['config'] = array(
            //The default date format for JSON and XML data is "%d-%m-%Y" http://docs.dhtmlx.com/gantt/desktop__loading.html#loadingfromadatabase
            'xml_date'=>'%Y-%m-%d',
            'columns'=>array_map(function($column){
                if ($column instanceof CCheckBoxColumn) {
                    $ret = array('name'=>$column->name);
                } elseif ($column instanceof AlxdStatusrefColumn) {
                    $ret = array('name'=>$column->name.($column->format ? '.'.$column->format : ''));
                } elseif ($column instanceof AlxdAttributerefColumn) {
                    $ret = array('name'=>$column->name.($column->attribute ? '.'.$column->attribute : ''));
                } else {
                    $ret = array('name'=>$column->name);
                }

                $r = new ReflectionMethod($column, 'renderHeaderCellContent');
                $r->setAccessible(true);
                ob_start();
                $r->invoke($column);
                $ret['label'] = ob_get_contents();
                ob_end_clean();

                if ($column instanceof CCheckBoxColumn) {
                    $ret['width'] = 36;
                } else {
                    $headerHtmlOptions = $column->headerHtmlOptions;
                    if (isset($headerHtmlOptions['style'])) {
                        $styles = explode(';', rtrim($headerHtmlOptions['style'], ';'));
                        foreach ($styles as $style) {
                            $pair = explode(':', $style, 2);
                            if (count($pair) == 2 && strtolower(trim($pair[0])) == 'width') {
                                $l = strlen($pair[1]);
                                if (strtolower(substr($pair[1], $l-2, 2)) == 'px') {
                                    $ret['width'] = substr($pair[1], 0, $l - 2);
                                }
                            }
                        }
                    }

                    if ($this->tree && $column->name == $this->tree_column_name) {
                        $ret['tree'] = $this->tree;
                    }
                }
                return $ret;
            }, $this->columns),
            'filters'=>array_map(function($column){
                $r = new ReflectionMethod($column, 'renderFilterCellContent');
                $r->setAccessible(true);
                ob_start();
                $r->invoke($column);
                $filter = ob_get_contents();
                ob_end_clean();

                return array(
                    'name'=>$column->name,
                    'control'=>$filter
                );
            }, $this->columns),
            'data'=>$this->getData(),
        );

        $options['config']['scale_unit'] = $this->scales[0]['unit'];
        $options['config']['date_scale'] = $this->scales[0]['date'];
        if (count($this->scales) > 1) {
            $options['config']['subscales'] = array_slice($this->scales, 1);
        }

        if ($this->filter !== null) {
            $options['config']['scale_height_auto'] = true;
            $options['config']['filter'] = true;
        }

        if (isset($this->dataProcessorUrl)) {
            $options['dataProcessorUrl'] = $this->dataProcessorUrl;
        }

        $options=CJavaScript::encode($options);
        $cs=Yii::app()->getClientScript();
        if ($this->_assets == null) {
            $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets';
            $this->_assets = Yii::app()->assetManager->publish($path);
        }

        $cs->registerCoreScript('jquery');
        $cs->registerCoreScript('bbq');

        if($this->enableHistory)
            $cs->registerCoreScript('history');

        $cs->registerCssFile($this->_assets . DIRECTORY_SEPARATOR . 'dhtmlxgantt.css');
        $cs->registerScriptFile($this->_assets . DIRECTORY_SEPARATOR . 'dhtmlxgantt.js', CClientScript::POS_BEGIN);
        $cs->registerScriptFile($this->_assets . DIRECTORY_SEPARATOR . 'locale/locale_'.Yii::app()->language.'.js', CClientScript::POS_BEGIN);
        $cs->registerScriptFile($this->_assets . DIRECTORY_SEPARATOR . 'alxd.dhtmlxgantt.js', CClientScript::POS_BEGIN);
        $cs->registerScript(__CLASS__.'#'.$id,"jQuery('#$id').alxdDhtmlxGantt($options);", CClientScript::POS_READY);
    }

Реализация на JavaScript

Мои первые попытки написать компоненту не создавая отдельного js модуля не увенчались успехом, что и к лучшему. Помучившись стало понятно, что надо написать полноценный js-модуль, который будет обрабатывать процесс инициализации DHTML Gantt, привязки событий и обработку переключения страниц и фильтра. Более того, как выяснилось позже, пришлось перекрыть пару методов для корректного render'а шапки и данных диаграммы. Выглядеть должно было как-то так (на картинке монтаж, чтобы показать и фильтр, и контекстное меню):
Прикручивание диаграммы Гантта - 3

Порывшись в исходном коде компоненты, нашел два метода: _render_grid_header и _render_grid_item. Попытался хирургически их перекрыть, но ничего не получилось, и в конечном счете полностью их перекрыл скопировав исходный код и внеся в него необходимые правки.

_render_grid_header и _render_grid_item

gantt._render_grid_header = function () {
    var columns = this.getGridColumns();
    var filters = this.config.filters;
    var title_cells = [];
    var filter_cells = [];
    var width = 0,
        labels = this.locale.labels;

    var lineHeigth = this.config.scale_height - 2;

    for (var i = 0; i < columns.length; i++) {
        var last = i == columns.length - 1;
        var col = columns[i];
        var colWidth = col.width*1;
        if (last && this._get_grid_width() > width + colWidth)
            col.width = colWidth = this._get_grid_width() - width;
        width += colWidth;
        var sort = (this._sort && col.name == this._sort.name) ? ("<div class='gantt_sort gantt_" + this._sort.direction + "'></div>") : "";
        var cssClass = ["gantt_grid_head_cell",
            ("gantt_grid_head_" + col.name),
            (last ? "gantt_last_cell" : ""),
            this.templates.grid_header_class(col.name, col)].join(" ");

        var style = "width:" + (colWidth - (last ? 1 : 0)) + "px;";
        var label = (col.label || labels["column_" + col.name]);
        label = label || "";
        var title_cell = "<div class='" + cssClass + "' style='" + style + "' column_id='" + col.name + "'>" + label + sort + "</div>";
        title_cells.push(title_cell);

        if (filters.length >= i) {
            var filter = filters[i];
            var filter_cell = "<div class='" + cssClass + "' style='" + style + "'>" + filter.control + "</div>";
            filter_cells.push(filter_cell);
        }
    }

    this.$grid_scale.innerHTML = "<div class='gantt_grid_scale_row'>" + title_cells.join("") + "</div>" + (this.config.filter ? "<div class='gantt_grid_scale_row'>" + filter_cells.join("") + "</div>" : "");
    this.$grid_scale.style.width = (width - 1) + "px";

    if (this.config.scale_height_auto == true) {
        var $grid_scale = $(this.$grid_scale);
        $grid_scale.removeAttr("style");
        this.config.scale_height = $grid_scale.height();
        this.$grid_scale.style.height = (this.config.scale_height - 1) + "px";
        this.$grid_scale.style.lineHeight = "1.42857143";
    } else {
        this.$grid_scale.style.height = (this.config.scale_height - 1) + "px";
        this.$grid_scale.style.lineHeight = lineHeigth + "px";
    }
};

gantt._render_grid_item = function (item) {
    var btn_cell_width = 20;
    if (!gantt._is_grid_visible())
        return null;

    var columns = this.getGridColumns();
    var cells = [];
    var width = 0;
    for (var i = 0; i < columns.length; i++) {
        var last = i == columns.length - 1;
        var col = columns[i];
        var cell;

        var value;
        var actions = null;
        if (col.template)
            value = col.template(item);
        else
            value = item[col.name];

        if (value.actions) {
            actions = value.actions;
            value = value.content;
        }

        if (value instanceof Date)
            value = this.templates.date_grid(value, item);

        value = "<div class='gantt_tree_content'>" + value + "</div>";
        var css = "gantt_cell" + (last ? " gantt_last_cell" : "");

        var tree = "";
        if (col.tree) {
            for (var j = 0; j < item.$level; j++)
                tree += this.templates.grid_indent(item);

            var has_child = this._has_children(item.id);
            if (has_child) {
                tree += this.templates.grid_open(item);
                tree += this.templates.grid_folder(item);
            } else {
                tree += this.templates.grid_blank(item);
                tree += this.templates.grid_file(item);
            }
        }
        var style = "width:" + (col.width - (actions ? btn_cell_width : 0) - (last ? 1 : 0)) + "px;";
        if (this.defined(col.align))
            style += "text-align:" + col.align + ";";
        cell = "<div class='" + css + "' style='" + style + "'>" + tree + value + "</div>";
        cells.push(cell);

        if (actions) {
            cells.push(actions);
        }
    }
    var css = item.$index % 2 === 0 ? "" : " odd";
    css += (item.$transparent) ? " gantt_transparent" : "";

    css += (item.$dataprocessor_class ? " " + item.$dataprocessor_class : "");

    if (this.templates.grid_row_class) {
        var css_template = this.templates.grid_row_class.call(this, item.start_date, item.end_date, item);
        if (css_template)
            css += " " + css_template;
    }

    if (this.getState().selected_task == item.id) {
        css += " gantt_selected";
    }
    var el = document.createElement("div");
    el.className = "gantt_row" + css;
    el.style.height = this.config.row_height + "px";
    el.style.lineHeight = (gantt.config.row_height) + "px";
    el.setAttribute(this.config.task_attribute, item.id);
    el.innerHTML = cells.join("");
    return el;
};

Собственно, код alxd.dhtmlxgantt.js [14] частично заимствовал из jquery.yiigridview.js [15] опять же, чтобы соблюсти преемственность конечного класса.

Стили

Конечно, из-за нахального перекрытия и изменения кода render'а DHTML Gantt, пришлось немного поправить стили. Не приложил их к своему исходному коду только потому, что в проекте easla.com [2] они хранятся в отдельном less-файле. Изменения следующие:

@btn-cell-width:  20px;

.gantt-loading {
  .gantt_container {
    background: url('../images/loading.gif') no-repeat center center !important;

    > .gantt_grid, > .gantt_task {
      opacity: 0.5;
    }
  }
}

.gantt_grid_scale, .gantt_task_scale {
  font-size: inherit;
  background-color: @primary-color;
}

.gantt_grid_head_cell {
  padding: 8px;
  text-align: inherit;
  overflow: inherit;
  white-space: normal;
}

.gantt_row {
  .btn-group {
    vertical-align: inherit;
  }

  .btn-cell {
    width: @btn-cell-width;
    height: 100%;

    .btn {
      height: inherit;
      line-height: inherit;
      padding: 0px;
      border-radius: 0px;
      border: none;
      width: 100%;

      span.caret {
        display: none;
      }
    }

    i {
      font-size: 14px;
    }
  }
}

.alxdgrid {
  .gantt.table-footer {
    margin-top: -1px;
  }
}

Применение

Использовать получившуюся компоненту можно также, как CGridView, только надо указать taskAttributes. В моем случае код выглядит вот так:

$cnt = $viewpub->provider->totalItemCount;

        $template = array();
        $template[] = '{items}';
        if ($cnt > 0) {
            if ($viewpub->getShowAll()) {
                $isShowAll = isset($_GET['showall']) && $_GET['showall'] == 1;
                $params = array_merge((array)'', $_GET);
                if ($isShowAll) {
                    unset($params['showall']);
                } else {
                    $params['showall'] = 1;
                }
                $templateShowAll = CHtml::link(
                    $isShowAll ? '<i class="fa fa-files-o"></i>' : '<i class="fa fa-file-o"></i>',
                    $params,
                    array(
                        'id'=>'Viewpub_page_to_all',
                        'class'=>'btn btn-primary btn-outline pull-right show-all',
                        'title'=>$isShowAll ? Yii::t('Viewpub','Page-by-page') : Yii::t('Viewpub','All at once')
                    )
                );
                $template[] = '<div class="gantt table-footer clearfix">' . $templateShowAll . '{pager}{summary}</div>';
            } else {
                $template[] = '<div class="gantt table-footer clearfix">{pager}{summary}</div>';
            }
        }

        $options = array(
            'id' => 'viewpub_grid_' . $suffix,
            'type' => BsHtml::GRID_TYPE_STRIPED,
            'dataProcessorUrl'=>Yii::app()->createUrl('viewpub/ganttDataProcessor', array('viewpub_id'=>$viewpub->id, 'user_id'=>$user->id)),
            'dataProvider' => $viewpub->provider,
            'filter' => $viewpub->objectref,
            'columns' => array_merge(
                $cntCommands ? array($checkBoxColumn) : array(),
                $viewpub->columns
            ),
            'taskAttributes'=> $viewpub->getTaskAttributes(),
            'itemsCssClass' => 'gantt-mono-primary',
            'summaryCssClass'=>'hidden-xs table-summary',
            'pagerCssClass'=>'table-pagination',
            'loadingCssClass'=>'gantt-loading',
            'enableSorting' => $viewpub->getSorting(),
            'tree' => $viewpub->getTree(),
            'scales' =>$viewpub->getScales(),
            'template' => implode('', $template),
            'pager' => array(
                'class' => 'CLinkPager',
                'maxButtonCount' => $isMobileClient ? 3 : 10,
                'firstPageLabel' => ' <i class="fa fa-angle-double-left"></i> ',
                'header' => '',
                'hiddenPageCssClass' => 'disabled',
                'lastPageLabel' => ' <i class="fa fa-angle-double-right"></i> ',
                'nextPageLabel' => ' <i class="fa fa-angle-right"></i> ',//'>',
                'selectedPageCssClass' => 'active',
                'prevPageLabel' => ' <i class="fa fa-angle-left"></i> ',//'<',
                'htmlOptions' => array('class' => 'pagination')
            ),
            'updateSelector' => ($viewpub->getShowAll() ? '{page}, {sort}, a.show-all' : '{page},{sort}'),
            'ajaxUpdateError'=>'function(request, textStatus, errorThrow, errorMessage){ EaslaAlert.add(request.status == 501 ? request.responseText : request.statusText+": "+extractExceptionText(request.responseText), {type: "danger"});}'
        );

        if ($cntCommands) {
            $options['afterAjaxUpdate'] = 'function() { $(":checkbox").uniform();}';
            $options['onTaskSelected'] = $options['onTaskOpened'] = $options['onTaskClosed'] = $options['onTaskDrag'] = 'function(id) { $(":checkbox").uniform();}';
        }

        $renderViewpub = $this->widget('ext.AlxdDhtmlxGantt.AlxdDhtmlxGantt', $options, true);

В easla.com [2] в переменной $viewpub хранится класс, который формирует все необходимые параметры для отображения вида. Но в общем случае:
dataProvider может быть как CActiveDataProvider [16], так и CArrayDataProvider [17];
сolumns тот же самый columns [18], что и в обычной CGridView.
Showall – параметр, который обрабатывается на стороне провайдера и выставляет параметр pagination=false, таким образом исключая постраничное отображения и требуя отображения всех данных.

Итоги

В настоящий момент компонента используется в easla.com [2], как один из способов отображения информации для процессов управления задачами. Более подробно про управление задачами было описано в моей статье [19].
Выглядит все это удовольствие вот так:
Прикручивание диаграммы Гантта - 4
По сути получился Microsoft Project, только, как метко заметил GarbageIntegrator [20], без навязанных бизнес-процессов, с неограниченным количеством полей и статусов, а главное, без надоевших багов.

Текущую версию AlxdDhtmlxGantt кому нужно может найти на github [21]. Буду рад, если она кому-то пригодится.

Автор: easla.com

Источник [22]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/129273

Ссылки в тексте:

[1] диаграммы Гантта: https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%B0%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B0_%D0%93%D0%B0%D0%BD%D1%82%D0%B0

[2] easla.com: http://easla.com

[3] многие другие: https://habrahabr.ru/company/devexpress/blog/103836/

[4] DHTMLX Gantt: http://docs.dhtmlx.com/gantt/

[5] CGridView: http://www.yiiframework.com/doc/api/1.1/CGridView

[6] CBaseListView: http://www.yiiframework.com/doc/api/1.1/CBaseListView

[7] документации: http://docs.dhtmlx.com/gantt/desktop__loading.html#specifyingdataproperties

[8] scale_unit: http://docs.dhtmlx.com/gantt/api__gantt_scale_unit_config.html

[9] date_scale: http://docs.dhtmlx.com/gantt/api__gantt_date_scale_config.html

[10] subscales: http://docs.dhtmlx.com/gantt/api__gantt_subscales_config.html

[11] renderDataCell: http://www.yiiframework.com/doc/api/1.1/CGridColumn#renderDataCell-detail

[12] renderDataCellContent: http://www.y iiframework.com/doc/api/1.1/CGridColumn#renderDataCellContent-detail

[13] ReflectionMethod: http://php.net/manual/ru/class.reflectionmethod.php

[14] alxd.dhtmlxgantt.js: https://github.com/Alxdhere/AlxdDhtmlxGantt/blob/master/assets/alxd.dhtmlxgantt.js

[15] jquery.yiigridview.js: https://github.com/yiisoft/yii/blob/master/framework/zii/widgets/assets/gridview/jquery.yiigridview.js

[16] CActiveDataProvider: http://www.yiiframework.com/doc/api/1.1/CActiveDataProvider

[17] CArrayDataProvider: http://www.yiiframework.com/doc/api/1.1/CArrayDataProvider

[18] columns: http://www.yiiframework.com/doc/api/1.1/CGridView#columns-detail

[19] статье: https://habrahabr.ru/company/easla/blog/301944/

[20] GarbageIntegrator: https://habrahabr.ru/users/garbageintegrator/

[21] github: https://github.com/Alxdhere/AlxdDhtmlxGantt

[22] Источник: https://habrahabr.ru/post/302786/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best