mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 01:43:47 +01:00
1292 lines
37 KiB
JavaScript
1292 lines
37 KiB
JavaScript
/**
|
|
* EGroupware eTemplate2 - JS widget for GANTT chart
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link http://www.egroupware.org
|
|
* @author Nathan Gray
|
|
* @copyright Nathan Gray 2014
|
|
* @version $Id$
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
/*egw:uses
|
|
jsapi.jsapi;
|
|
jquery.jquery;
|
|
/phpgwapi/js/dhtmlxtree/codebase/dhtmlxcommon.js; // otherwise gantt breaks
|
|
/phpgwapi/js/dhtmlxGantt/codebase/dhtmlxgantt.js;
|
|
et2_core_inputWidget;
|
|
*/
|
|
|
|
/**
|
|
* Gantt chart
|
|
*
|
|
* The gantt widget allows children, which are displayed as a header. Any child input
|
|
* widgets are bound as live filters on existing data. The filter is done based on
|
|
* widget ID, such that the value of the widget must match that attribute in the task
|
|
* or the task will not be displayed. There is special handling for
|
|
* date widgets with IDs 'start_date' and 'end_date' to filter as an inclusive range
|
|
* instead of simple equality.
|
|
*
|
|
* @see http://docs.dhtmlx.com/gantt/index.html
|
|
* @augments et2_valueWidget
|
|
*/
|
|
var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
|
|
{
|
|
// Filters are inside gantt namespace
|
|
createNamespace: true,
|
|
|
|
attributes: {
|
|
"autoload": {
|
|
"name": "Autoload",
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "JSON URL or menuaction to be called for projects with no, GET parameter selected contains id"
|
|
},
|
|
"ajax_update": {
|
|
"name": "AJAX update method",
|
|
"type": "string",
|
|
"default": "",
|
|
"description": "AJAX menuaction to be called when the user changes a task. The function should take two parameters: the updated element, and all template values."
|
|
},
|
|
"duration_unit": {
|
|
"name": "Duration unit",
|
|
"type": "string",
|
|
"default": "minute",
|
|
"description": "The unit for task duration values. One of minute, hour, week, year."
|
|
},
|
|
columns: {
|
|
name: "Columns",
|
|
type: "any",
|
|
default: [
|
|
{name: "text", label: egw.lang('Title'), tree: true, width: '*'}
|
|
],
|
|
description: "Columns for the grid portion of the gantt chart. An array of objects with keys name, label, etc. See http://docs.dhtmlx.com/gantt/api__gantt_columns_config.html"
|
|
},
|
|
value: {type: 'any'},
|
|
needed: {ignore: true},
|
|
onfocus: {ignore: true},
|
|
tabindex: {ignore: true}
|
|
},
|
|
|
|
// Common configuration for Egroupware/eTemplate
|
|
gantt_config: {
|
|
// Gantt takes a different format of date format, all the placeholders are prefixed with '%'
|
|
api_date: '%Y-%n-%d %H:%i:%s',
|
|
xml_date: '%Y-%n-%d %H:%i:%s',
|
|
|
|
// Duration is a unitless field. This is the unit.
|
|
duration_unit: 'minute',
|
|
duration_step: 1,
|
|
|
|
show_progress: true,
|
|
order_branch: false,
|
|
min_column_width: 30,
|
|
min_grid_column_width: 30,
|
|
task_height: 25,
|
|
fit_tasks: true,
|
|
autosize: '',
|
|
// Date rounding happens either way, but this way it rounds to the displayed grid resolution
|
|
// Also avoids a potential infinite loop thanks to how the dates are rounded with false
|
|
round_dnd_dates: false,
|
|
// Round resolution
|
|
time_step: parseInt(this.egw().preference('interval','calendar') || 15),
|
|
min_duration: 1 * 60 * 1000, // 1 minute in ms
|
|
|
|
scale_unit: 'day',
|
|
date_scale: '%d',
|
|
subscales: [
|
|
{unit:"month", step:1, date:"%F, %Y"},
|
|
//{unit:"hour", step:1, date:"%G"}
|
|
],
|
|
columns: [
|
|
{name: "text", label: egw.lang('Title'), tree: true, width: '*'}
|
|
]
|
|
},
|
|
|
|
init: function(_parent, _attrs) {
|
|
// _super.apply is responsible for the actual setting of the params (some magic)
|
|
this._super.apply(this, arguments);
|
|
|
|
// Gantt instance
|
|
this.gantt = null;
|
|
|
|
// DOM Nodes
|
|
this.filters = $j(document.createElement("div"))
|
|
.addClass('et2_gantt_header');
|
|
this.gantt_node = $j('<div style="width:100%;height:100%"></div>');
|
|
this.htmlNode = $j(document.createElement("div"))
|
|
.css('height', this.options.height)
|
|
.addClass('et2_gantt');
|
|
|
|
this.htmlNode.prepend(this.filters);
|
|
this.htmlNode.append(this.gantt_node);
|
|
|
|
// Create the dynheight component which dynamically scales the inner
|
|
// container.
|
|
this.dynheight = new et2_dynheight(
|
|
this.getParent().getDOMNode(this.getParent()) || this.getInstanceManager().DOMContainer,
|
|
this.gantt_node, 300
|
|
);
|
|
|
|
this.setDOMNode(this.htmlNode[0]);
|
|
},
|
|
|
|
destroy: function() {
|
|
if(this.gantt !== null)
|
|
{
|
|
// Unselect task before removing it, or we get errors later if it is accessed
|
|
this.gantt.unselectTask();
|
|
this.gantt.detachAllEvents();
|
|
this.gantt.clearAll();
|
|
this.gantt = null;
|
|
|
|
// Destroy dynamic full-height
|
|
if(this.dynheight) this.dynheight.free();
|
|
|
|
this._super.apply(this, arguments);}
|
|
|
|
this.htmlNode.remove();
|
|
this.htmlNode = null;
|
|
},
|
|
|
|
doLoadingFinished: function() {
|
|
this._super.apply(this, arguments);
|
|
if(this.gantt != null) return false;
|
|
|
|
var config = jQuery.extend({}, this.gantt_config);
|
|
|
|
// Set initial values for start and end, if those filters exist
|
|
var start_date = this.getWidgetById('start_date');
|
|
var end_date = this.getWidgetById('end_date');
|
|
if(start_date)
|
|
{
|
|
config.start_date = start_date.getValue() ? new Date(start_date.getValue() * 1000) : null;
|
|
}
|
|
if(end_date)
|
|
{
|
|
config.end_date = end_date.getValue() ? new Date(end_date.getValue() * 1000): null;
|
|
}
|
|
if(this.options.duration_unit)
|
|
{
|
|
config.duration_unit = this.options.duration_unit;
|
|
}
|
|
|
|
// Initialize chart
|
|
this.gantt = this.gantt_node.dhx_gantt(config);
|
|
|
|
if(this.options.zoom)
|
|
{
|
|
this.set_zoom(this.options.zoom);
|
|
}
|
|
|
|
if(this.options.value)
|
|
{
|
|
this.set_value(this.options.value);
|
|
}
|
|
if(this.options.columns)
|
|
{
|
|
this.set_columns(this.options.columns);
|
|
}
|
|
|
|
// Update start & end dates with chart values for consistency
|
|
if(start_date && this.options.value.data && this.options.value.data.length)
|
|
{
|
|
start_date.set_value(this.gantt.getState().min_date);
|
|
}
|
|
if(end_date && this.options.value.data && this.options.value.data.length)
|
|
{
|
|
end_date.set_value(this.gantt.getState().max_date);
|
|
}
|
|
|
|
// Bind some events to make things nice and et2
|
|
this._bindGanttEvents();
|
|
|
|
// Bind filters
|
|
this._bindChildren();
|
|
|
|
return true;
|
|
},
|
|
|
|
getDOMNode: function(_sender) {
|
|
// Return filter container for children
|
|
if (_sender != this && this._children.indexOf(_sender) != -1)
|
|
{
|
|
return this.filters[0];
|
|
}
|
|
|
|
// Normally simply return the main div
|
|
return this._super.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Implement the et2_IResizable interface to resize
|
|
*/
|
|
resize: function()
|
|
{
|
|
if(this.dynheight)
|
|
{
|
|
this.dynheight.update(function(w,h) {
|
|
if(this.gantt)
|
|
{
|
|
this.gantt.setSizes();
|
|
}
|
|
}, this);
|
|
}
|
|
else
|
|
{
|
|
this.gantt.setSizes();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Changes the units for duration
|
|
* @param {string} duration_unit One of minute, hour, week, year
|
|
*/
|
|
set_duration_unit: function(duration_unit)
|
|
{
|
|
this.options.duration_unit = duration_unit;
|
|
if(this.gantt && this.gantt.config.duration_unit != duration_unit)
|
|
{
|
|
this.gantt.config.duration_unit = duration_unit;
|
|
// Clear the end date, or previous end date may break time scale
|
|
this.gantt.config.end_date = null;
|
|
this.gantt.refreshData();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set the columns for the grid (left) portion
|
|
*
|
|
* @param {Object[]} columns - A list of columns
|
|
* @param {string} columns[].name The column's ID
|
|
* @param {string} columns[].label The title for the column header
|
|
* @param {string} columns[].width Width of the column
|
|
*
|
|
* @see http://docs.dhtmlx.com/gantt/api__gantt_columns_config.html for full options
|
|
*/
|
|
set_columns: function(columns)
|
|
{
|
|
this.gantt_config.columns = columns;
|
|
|
|
var displayed_columns = [];
|
|
var gantt_widget = this;
|
|
|
|
// Make sure there's enough room for them all
|
|
var width = 0;
|
|
for(var col in columns)
|
|
{
|
|
// Preserve original width, gantt will resize column to fit
|
|
if(!columns[col]._width)
|
|
{
|
|
columns[col]._width = columns[col].width;
|
|
}
|
|
columns[col].width = columns[col]._width;
|
|
if(!columns[col].template)
|
|
{
|
|
// Use an et2 widget to render the column value, if one was provided
|
|
// otherwise, just display the value
|
|
columns[col].template = function(task) {
|
|
var value = typeof task[this.name] == 'undefined'||task[this.name] == null ? '':task[this.name];
|
|
|
|
// No value, but there's a project title. Try reading the project value.
|
|
if(!value && this.name.indexOf('pe_') == 0 && task.pm_title)
|
|
{
|
|
var pm_col = this.name.replace('pe_','pm_');
|
|
value = typeof task[pm_col] == 'undefined' || task[pm_col] == null ? '':task[pm_col];
|
|
}
|
|
if(this.widget && typeof this.widget == 'string')
|
|
{
|
|
var attrs = jQuery.extend({readonly:true}, this.widget_attributes||{});
|
|
this.widget = et2_createWidget(this.widget, attrs, gantt_widget);
|
|
}
|
|
if (this.widget)
|
|
{
|
|
this.widget.set_value(value);
|
|
value = $j(this.widget.getDOMNode()).html();
|
|
}
|
|
return '<div class="gantt_column_'+this.name+'">' + value + '</div>';
|
|
};
|
|
}
|
|
|
|
// Actual hiding is available in the pro version of gantt chart
|
|
if(!columns[col].hide)
|
|
{
|
|
displayed_columns.push(columns[col]);
|
|
width += parseInt(columns[col]._width) || 0;
|
|
}
|
|
|
|
}
|
|
// Add in add column
|
|
displayed_columns.push({name: 'add', width: 26});
|
|
width += 26;
|
|
|
|
if(width != this.gantt_config.grid_width || typeof this.gantt_config.grid_width == 'undefined')
|
|
{
|
|
this.gantt_config.grid_width = Math.min(Math.max(200, width), this.htmlNode.width());
|
|
}
|
|
|
|
if(this.gantt == null) return;
|
|
this.gantt.config.columns = displayed_columns;
|
|
this.gantt.config.grid_width = this.gantt_config.grid_width;
|
|
this.gantt.render();
|
|
},
|
|
|
|
/**
|
|
* Sets the data to be displayed in the gantt chart.
|
|
*
|
|
* Data is a JSON object with 'data' and 'links', both of which are arrays.
|
|
* {
|
|
* data:[
|
|
* {id:1, text:"Project #1", start_date:"01-04-2013", duration:18},
|
|
* {id:2, text:"Task #1", start_date:"02-04-2013", duration:8, parent:1},
|
|
* {id:3, text:"Task #2", start_date:"11-04-2013", duration:8, parent:1}
|
|
* ],
|
|
* links:[
|
|
* {id:1, source:1, target:2, type:"1"},
|
|
* {id:2, source:2, target:3, type:"0"}
|
|
* ]
|
|
* // Optional:
|
|
* zoom: 1-4,
|
|
*
|
|
* };
|
|
* Any additional data can be included and used, but the above is the minimum
|
|
* required data.
|
|
*
|
|
* @see http://docs.dhtmlx.com/gantt/desktop__loading.html
|
|
*/
|
|
set_value: function(value) {
|
|
if(this.gantt == null) return false;
|
|
|
|
// Unselect task before removing it, or we get errors later if it is accessed
|
|
this.gantt.unselectTask();
|
|
|
|
// Clear previous value
|
|
this.gantt.clearAll();
|
|
|
|
// Clear the end date, or previous end date may break time scale
|
|
this.gantt.config.end_date = null;
|
|
|
|
if(value.duration_unit)
|
|
{
|
|
this.set_duration_unit(value.duration_unit);
|
|
}
|
|
|
|
this.gantt.showCover();
|
|
|
|
// Set zoom to max, in case data spans a large time
|
|
this.set_zoom(value.zoom || 5);
|
|
|
|
// Wait until zoom is done before continuing so timescales are done
|
|
var gantt_widget = this;
|
|
var zoom_wait = this.gantt.attachEvent('onGanttRender', function() {
|
|
this.detachEvent(zoom_wait);
|
|
|
|
// Ensure proper format, no extras
|
|
var safe_value = {
|
|
data: value.data || [],
|
|
links: value.links || []
|
|
};
|
|
this.config.start_date = value.start_date || null;
|
|
this.config.end_date = value.end_date || null;
|
|
this.parse(safe_value);
|
|
|
|
gantt_widget._apply_sort();
|
|
gantt_widget.gantt_loading = false;
|
|
// Once we force the start / end date (below), gantt won't recalculate
|
|
// them if the user clears the date, so we store them and use them
|
|
// if the user clears the date.
|
|
//gantt_widget.stored_state = jQuery.extend({},this.getState());
|
|
|
|
// Doing this again here forces the gantt chart to trim the tasks
|
|
// to fit the date range, rather than drawing all the dates out
|
|
// to the start date.
|
|
// No speed improvement, but it makes a lot more sense in the UI
|
|
var range = this.attachEvent('onGanttRender', function() {
|
|
this.detachEvent(range);
|
|
if(value.start_date || value.end_date)
|
|
{
|
|
// TODO: Some weirdness in this when changing dates
|
|
// If this is done, gantt does not respond when user clears the start date
|
|
/*
|
|
this.refreshData();
|
|
debugger;
|
|
if(gantt_widget.getWidgetById('start_date') && new Date(value.start_date) > this._min_date)
|
|
{
|
|
gantt_widget.getWidgetById('start_date').set_value(value.start_date || null);
|
|
}
|
|
if(gantt_widget.getWidgetById('end_date') && new Date(value.end_date) < this._max_date)
|
|
{
|
|
gantt_widget.getWidgetById('end_date').set_value(value.end_date || null);
|
|
}
|
|
this.refreshData();
|
|
this.render();
|
|
*/
|
|
|
|
this.scrollTo(this.posFromDate(new Date(value.end_date || value.start_date )),0);
|
|
}
|
|
|
|
// Zoom to specified or auto level
|
|
var auto_zoom = this.attachEvent('onGanttRender', function() {
|
|
this.detachEvent(auto_zoom);
|
|
|
|
var old_zoom;
|
|
|
|
// Zooming out re-scales the gantt start & end dates and
|
|
// changes what values they can have,
|
|
// so to zoom in we have to do it step by step
|
|
do
|
|
{
|
|
this.render();
|
|
old_zoom = gantt_widget.options.zoom;
|
|
gantt_widget.set_zoom(value.zoom || false);
|
|
} while(gantt_widget.options.zoom != old_zoom)
|
|
this.hideCover();
|
|
|
|
if(console.timeEnd) console.timeEnd("Gantt set_value");
|
|
if(console.groupEnd) console.groupEnd();
|
|
if(console.profile) console.profileEnd();
|
|
});
|
|
});
|
|
// This render re-calculates start/end dates
|
|
// this.render();
|
|
});
|
|
|
|
// This render re-sizes gantt to work at highest zoom
|
|
this.gantt.render();
|
|
},
|
|
/**
|
|
* getValue has to return the value of the input widget
|
|
*/
|
|
getValue: function() {
|
|
return jQuery.extend({}, this.value, {
|
|
zoom: this.options.zoom,
|
|
duration_unit: this.gantt.config.duration_unit
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Refresh given tasks for specified change
|
|
*
|
|
* Change type parameters allows for quicker refresh then complete server side reload:
|
|
* - update: request just modified data for given tasks
|
|
* - edit: same as edit
|
|
* - delete: just delete the given tasks clientside (no server interaction neccessary)
|
|
* - add: requires full reload
|
|
*
|
|
* @param {string[]|string} _task_ids tasks to refresh
|
|
* @param {?string} _type "update", "edit", "delete" or "add"
|
|
*
|
|
* @see jsapi.egw_refresh()
|
|
* @fires refresh from the widget itself
|
|
*/
|
|
refresh: function(_task_ids, _type) {
|
|
// Framework trying to refresh, but gantt not fully initialized
|
|
if(!this.gantt || !this.gantt_node || !this.options.autoload) return;
|
|
|
|
// Sanitize arguments
|
|
if (typeof _type == 'undefined') _type = 'edit';
|
|
if (typeof _task_ids == 'string' || typeof _task_ids == 'number') _task_ids = [_task_ids];
|
|
if (typeof _task_ids == "undefined" || _task_ids === null)
|
|
{
|
|
// Use the root
|
|
_task_ids = this.gantt._branches[0];
|
|
}
|
|
|
|
id_loop:
|
|
for(var i = 0; i < _task_ids.length; i++)
|
|
{
|
|
var task = this.gantt.getTask(_task_ids[i]);
|
|
if(!task) _type = null;
|
|
switch(_type)
|
|
{
|
|
case "edit":
|
|
case "update":
|
|
var value = this.getInstanceManager().getValues(this.getInstanceManager().widgetContainer);
|
|
this.gantt.showCover();
|
|
this.egw().json(this.options.autoload,
|
|
[_task_ids[i],value,task.parent||false],
|
|
function(data) {
|
|
this.gantt.parse(data);
|
|
this._apply_sort();
|
|
this.gantt.hideCover();
|
|
},
|
|
this,true,this
|
|
).sendRequest();
|
|
break;
|
|
case "delete":
|
|
this.gantt.deleteTask(_task_ids[i]);
|
|
break;
|
|
case "add":
|
|
var data = null;
|
|
if(data = this.egw().dataGetUIDdata(_task_ids[i]) && data.data)
|
|
{
|
|
this.gantt.parse(data.data);
|
|
this._apply_sort();
|
|
}
|
|
else
|
|
{
|
|
// Refresh the whole thing
|
|
this.refresh();
|
|
break id_loop;
|
|
}
|
|
break;
|
|
default:
|
|
// Refresh the whole thing
|
|
this.refresh();
|
|
}
|
|
}
|
|
|
|
// Trigger an event so app code can act on it
|
|
$j(this).triggerHandler("refresh",[this,_task_ids,_type]);
|
|
},
|
|
|
|
/**
|
|
* Is dirty returns true if the value of the widget has changed since it
|
|
* was loaded.
|
|
*/
|
|
isDirty: function() {
|
|
return this.value != null;
|
|
},
|
|
|
|
/**
|
|
* Causes the dirty flag to be reseted.
|
|
*/
|
|
resetDirty: function() {
|
|
this.value = null;
|
|
},
|
|
|
|
/**
|
|
* Checks the data to see if it is valid, as far as the client side can tell.
|
|
* Return true if it's not possible to tell on the client side, because the server
|
|
* will have the chance to validate also.
|
|
*
|
|
* The messages array is to be populated with everything wrong with the data,
|
|
* so don't stop checking after the first problem unless it really makes sense
|
|
* to ignore other problems.
|
|
*
|
|
* @param {String[]} messages List of messages explaining the failure(s).
|
|
* messages should be fairly short, and already translated.
|
|
*
|
|
* @return {boolean} True if the value is valid (enough), false to fail
|
|
*/
|
|
isValid: function(messages) {return true},
|
|
|
|
/**
|
|
* Set a URL to fetch the data from the server.
|
|
* Data must be in the specified format.
|
|
* @see http://docs.dhtmlx.com/gantt/desktop__loading.html
|
|
*/
|
|
set_autoload: function(url) {
|
|
if(this.gantt == null) return false;
|
|
this.options.autoloading = url;
|
|
|
|
throw new Exception('Not implemented yet - apparently loading segments is not supported automatically');
|
|
},
|
|
|
|
/**
|
|
* Sets the level of detail for the chart, which adjusts the scale(s) across the
|
|
* top and the granularity of the drag grid.
|
|
*
|
|
* Gantt chart needs a render() after changing.
|
|
*
|
|
* @param {int} level Higher levels show more grid, at larger granularity.
|
|
* @return {int} Current level
|
|
*/
|
|
set_zoom: function(level) {
|
|
|
|
var subscales = [];
|
|
var scale_unit = 'day';
|
|
var date_scale = '%d';
|
|
var step = 1;
|
|
var time_step = this.gantt_config.time_step;
|
|
var min_column_width = this.gantt_config.min_column_width;
|
|
|
|
// No level? Auto calculate.
|
|
if(level > 5) level = 5;
|
|
if(!level || level < 1) {
|
|
// Make sure we have the most up to date info for the calculations
|
|
// There may be a more efficient way to trigger this though
|
|
try {
|
|
this.gantt.refreshData();
|
|
}
|
|
catch (e)
|
|
{}
|
|
|
|
var difference = (this.gantt.getState().max_date - this.gantt.getState().min_date)/1000; // seconds
|
|
// Spans more than 3 years
|
|
if(difference > 94608000)
|
|
{
|
|
level = 5;
|
|
}
|
|
// Spans more than 3 months
|
|
else if(difference > 7776000)
|
|
{
|
|
level = 4;
|
|
}
|
|
// More than 3 days
|
|
else if(difference > 86400 * 3)
|
|
{
|
|
level = 3;
|
|
}
|
|
// More than 1 day
|
|
else
|
|
{
|
|
level = 2;
|
|
}
|
|
}
|
|
|
|
// Adjust Gantt settings for specified level
|
|
switch(level)
|
|
{
|
|
case 5:
|
|
// Several years
|
|
//subscales.push({unit: "year", step: 1, date: '%Y'});
|
|
scale_unit = 'year';
|
|
date_scale = '%Y';
|
|
break;
|
|
case 4:
|
|
// A year or more, scale in weeks
|
|
subscales.push({unit: "month", step: 1, date: '%F %Y'});
|
|
scale_unit = 'week';
|
|
date_scale= '#%W';
|
|
break;
|
|
case 3:
|
|
// Less than a year, several months
|
|
subscales.push({unit: "month", step: 1, date: '%F %Y', class: 'et2_clickable'});
|
|
break;
|
|
case 2:
|
|
default:
|
|
// About a month
|
|
subscales.push({unit: "day", step: 1, date: '%F %d'});
|
|
scale_unit = 'hour';
|
|
date_scale = this.egw().preference('timeformat') == '24' ? "%G" : "%g";
|
|
break;
|
|
case 1: // A day or two, scale in Minutes
|
|
subscales.push({unit: "day", step: 1, date: '%F %d'});
|
|
date_scale = this.egw().preference('timeformat') == '24' ? "%G:%i" : "%g:%i";
|
|
step = parseInt(this.egw().preference('interval','calendar') || 15);
|
|
time_step = 1;
|
|
scale_unit = 'minute';
|
|
min_column_width = 50;
|
|
break;
|
|
}
|
|
|
|
// Apply settings
|
|
this.gantt.config.subscales = subscales;
|
|
this.gantt.config.scale_unit = scale_unit;
|
|
this.gantt.config.date_scale = date_scale;
|
|
this.gantt.config.step = step;
|
|
this.gantt.config.time_step = time_step;
|
|
this.gantt.config.min_column_width = min_column_width;
|
|
|
|
this.options.zoom = level;
|
|
|
|
this.gantt.refreshData();
|
|
return level;
|
|
},
|
|
|
|
/**
|
|
* Apply user's sort preference
|
|
*/
|
|
_apply_sort: function()
|
|
{
|
|
switch(egw.preference('gantt_pm_elementbars_order','projectmanager'))
|
|
{
|
|
case "pe_start":
|
|
case "pe_start,pe_end":
|
|
this.gantt.sort('start_date',false);
|
|
break;
|
|
case "pe_end":
|
|
this.gantt.sort('end_date',false);
|
|
break;
|
|
case 'pe_title':
|
|
this.gantt.sort('pe_title',false);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Exports the gantt chart to an external service that generates a file.
|
|
*
|
|
* @param {string} to One of PDF or PNG
|
|
* @returns {undefined}
|
|
*/
|
|
_export: function(to) {
|
|
var w = egw.open_link(egw.link('/etemplate/gantt_print.php'),'_blank','400x400');
|
|
var self = this;
|
|
jQuery(w).load(function() {
|
|
w.egw_LAB.wait(function() {
|
|
w.gantt = jQuery.extend(true,{},self.gantt);
|
|
|
|
// Loading it twice breaks export
|
|
if(typeof w.gantt.exportToPNG == 'undefined')
|
|
{
|
|
w.egw_LAB.script("https://export.dhtmlx.com/gantt/api.js");
|
|
}
|
|
w.egw_LAB.wait(function() {
|
|
$j(w.gantt.$container).parent().clone().appendTo(w.document.body);
|
|
// Custom CSS - just send it all
|
|
var css = '';
|
|
$j("link[type='text/css']").each(function() {css += this.outerHTML;});
|
|
|
|
var options = {
|
|
name: (w.gantt.getTask(w.gantt._order[0]).text||'gantt').replace(/ /g,'_')+'.'+to.toLowerCase(),
|
|
header: css + egw.config('site_title','phpgwapi'),
|
|
footer: $j('#egw_fw_footer',w.opener).html(),
|
|
// Doesn't work, export never happens:
|
|
// callback: function() {w.setTimeout(function() {w.close();}, 5000);}
|
|
};
|
|
console.log(options);
|
|
switch(to)
|
|
{
|
|
case 'PNG':
|
|
w.gantt.exportToPNG(options);
|
|
break;
|
|
case 'PDF':
|
|
w.gantt.exportToPDF(options);
|
|
}
|
|
w.setTimeout(function() {w.close();}, 5000);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* Exports the gantt chart do dhtmlx's external service, and makes a PDF
|
|
*/
|
|
exportToPDF: function() {
|
|
this._export('PDF');
|
|
},
|
|
|
|
/**
|
|
* Exports the gantt chart do dhtmlx's external service, and makes a PNG
|
|
*/
|
|
exportToPNG: function() {
|
|
this._export('PNG');
|
|
},
|
|
|
|
/**
|
|
* Bind all the internal gantt events for nice widget actions
|
|
*/
|
|
_bindGanttEvents: function() {
|
|
var gantt_widget = this;
|
|
|
|
// After the chart renders, resize to make sure it's all showing
|
|
this.gantt.attachEvent("onGanttRender", function() {
|
|
// Timeout gets around delayed rendering
|
|
window.setTimeout(function() {
|
|
gantt_widget.resize();
|
|
},100);
|
|
});
|
|
|
|
// Click on scale to zoom - top zooms out, bottom zooms in
|
|
this.gantt_node.on('click','.gantt_scale_line', function(e) {
|
|
var current_position = e.target.offsetLeft / $j(e.target.parentNode).width();
|
|
|
|
// Some crazy stuff make sure timing is OK to scroll after re-render
|
|
// TODO: Make this more consistently go to where you click
|
|
var id = gantt_widget.gantt.attachEvent("onGanttRender", function() {
|
|
gantt_widget.gantt.detachEvent(id);
|
|
gantt_widget.gantt.scrollTo(parseInt($j('.gantt_task_scale',gantt_widget.gantt_node).width() *current_position),0);
|
|
window.setTimeout(function() {
|
|
gantt_widget.gantt.scrollTo(parseInt($j('.gantt_task_scale',gantt_widget.gantt_node).width() *current_position),0);
|
|
},100);
|
|
});
|
|
|
|
if(this.parentNode && this.parentNode.firstChild == this && this.parentNode.childElementCount > 1)
|
|
{
|
|
// Zoom out
|
|
gantt_widget.set_zoom(gantt_widget.options.zoom + 1);
|
|
gantt_widget.gantt.render();
|
|
}
|
|
else if (gantt_widget.options.zoom > 1)
|
|
{
|
|
// Zoom in
|
|
gantt_widget.set_zoom(gantt_widget.options.zoom - 1);
|
|
gantt_widget.gantt.render();
|
|
}
|
|
/*
|
|
window.setTimeout(function() {
|
|
console.log("Scroll to");
|
|
gantt_widget.gantt.scrollTo(parseInt($j('.gantt_task_scale',gantt_widget.gantt_node).width() *current_position),0);
|
|
},50);
|
|
*/
|
|
});
|
|
|
|
this.gantt.attachEvent("onGridHeaderClick", function(column_name, e) {
|
|
if(column_name === "add")
|
|
{
|
|
gantt_widget._column_selection(e);
|
|
}
|
|
});
|
|
this.gantt.attachEvent("onContextMenu",function(taskId, linkId, e) {
|
|
if(gantt_widget.options.readonly) return false;
|
|
if(taskId)
|
|
{
|
|
gantt_widget._link_task(taskId);
|
|
}
|
|
else if (linkId)
|
|
{
|
|
this._delete_link_handler(linkId,e)
|
|
e.stopPropagation();
|
|
}
|
|
return false;
|
|
})
|
|
// Double click
|
|
this.gantt.attachEvent("onBeforeLightbox", function(id) {
|
|
gantt_widget._link_task(id);
|
|
// Don't do gantt default actions, actions handle it
|
|
return false;
|
|
});
|
|
|
|
// Update server after dragging a task
|
|
this.gantt.attachEvent("onAfterTaskDrag", function(id, mode, e) {
|
|
if(gantt_widget.options.readonly) return false;
|
|
|
|
// Round to nearest 10%
|
|
var task = this.getTask(id);
|
|
if(mode==="progress")
|
|
{
|
|
task.progress = Math.round(task.progress * 10) / 10;
|
|
this.updateTask(task.id);
|
|
}
|
|
|
|
var task = jQuery.extend({},this.getTask(id));
|
|
|
|
// Gantt chart deals with dates as Date objects, format as server likes
|
|
var date_parser = this.date.date_to_str(this.config.api_date);
|
|
if(task.start_date) task.start_date = date_parser(task.start_date);
|
|
if(task.end_date) task.end_date = date_parser(task.end_date);
|
|
|
|
var value = gantt_widget.getInstanceManager().getValues(gantt_widget.getInstanceManager().widgetContainer);
|
|
|
|
var set = true;
|
|
if(gantt_widget.options.onchange)
|
|
{
|
|
e.data = {task: task, mode: mode, value: value};
|
|
set = gantt_widget.change(e, gantt_widget);
|
|
}
|
|
if(gantt_widget.options.ajax_update && set)
|
|
{
|
|
var request = gantt_widget.egw().json(gantt_widget.options.ajax_update,
|
|
[task, mode, value]
|
|
).sendRequest(true);
|
|
}
|
|
});
|
|
|
|
// Update server for links
|
|
var link_update = function(id, link) {
|
|
if(gantt_widget.options.readonly) return false;
|
|
if(gantt_widget.options.ajax_update)
|
|
{
|
|
link.parent = this.getTask(link.source).parent;
|
|
var value = gantt_widget.getInstanceManager().getValues(gantt_widget.getInstanceManager().widgetContainer);
|
|
|
|
var request = gantt_widget.egw().json(gantt_widget.options.ajax_update,
|
|
[link,value], function(new_id) {
|
|
if(new_id)
|
|
{
|
|
link.id = new_id;
|
|
}
|
|
}
|
|
).sendRequest(true);
|
|
}
|
|
};
|
|
this.gantt.attachEvent("onAfterLinkAdd", link_update);
|
|
this.gantt.attachEvent("onAfterLinkDelete", link_update);
|
|
|
|
// Bind AJAX for dynamic expansion
|
|
// TODO: This could be improved
|
|
this.gantt.attachEvent("onTaskOpened", function(id, item) {
|
|
gantt_widget.refresh(id);
|
|
});
|
|
|
|
// Filters
|
|
this.gantt.attachEvent("onBeforeTaskDisplay", function(id, task) {
|
|
var display = true;
|
|
gantt_widget.iterateOver(function(_widget){
|
|
switch(_widget.id)
|
|
{
|
|
// Start and end date are an interval. Also update the chart to
|
|
// display those dates. Special handling because date widgets give
|
|
// value in timestamp (seconds), gantt wants Date object (ms)
|
|
case 'end_date':
|
|
if(_widget.getValue())
|
|
{
|
|
display = display && ((task['start_date'].valueOf() / 1000) < (new Date(_widget.getValue()).valueOf() / 1000) + 86400 );
|
|
}
|
|
return;
|
|
case 'start_date':
|
|
// End date is not actually a required field, so accept undefined too
|
|
if(_widget.getValue())
|
|
{
|
|
display = display && (typeof task['end_date'] == 'undefined' || !task['end_date'] || ((task['end_date'].valueOf() / 1000) >= (new Date(_widget.getValue()).valueOf() / 1000)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Regular equality comparison
|
|
if(_widget.getValue() && typeof task[_widget.id] != 'undefined')
|
|
{
|
|
if (task[_widget.id] != _widget.getValue())
|
|
{
|
|
display = false;
|
|
}
|
|
// Special comparison for objects, any intersection is a match
|
|
if(!display && typeof task[_widget.id] == 'object' || typeof _widget.getValue() == 'object')
|
|
{
|
|
var a = typeof task[_widget.id] == 'object' ? task[_widget.id] : _widget.getValue();
|
|
var b = a == task[_widget.id] ? _widget.getValue() : task[_widget.id];
|
|
if(typeof b == 'object')
|
|
{
|
|
display = jQuery.map(a,function(x) {
|
|
return jQuery.inArray(x,b) >= 0;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
display = jQuery.inArray(b,a) >= 0;
|
|
}
|
|
}
|
|
}
|
|
},gantt_widget, et2_inputWidget);
|
|
return display;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Bind onchange for any child input widgets
|
|
*/
|
|
_bindChildren: function() {
|
|
var gantt_widget = this;
|
|
this.iterateOver(function(_widget){
|
|
if(_widget.instanceOf(et2_gantt)) return;
|
|
// Existing change function
|
|
var widget_change = _widget.change;
|
|
|
|
var change = function(_node) {
|
|
// Call previously set change function
|
|
var result = widget_change.call(_widget,_node);
|
|
|
|
// Update filters
|
|
if(result) {
|
|
// Update dirty
|
|
_widget._oldValue = _widget.getValue();
|
|
|
|
// Start date & end date change the display
|
|
if(_widget.id == 'start_date' || _widget.id == 'end_date')
|
|
{
|
|
var start = this.getWidgetById('start_date');
|
|
var end = this.getWidgetById('end_date');
|
|
gantt_widget.gantt.config.start_date = start && start.getValue() ? new Date(start.getValue()) : gantt_widget.gantt.getState().min_date;
|
|
// End date is inclusive
|
|
gantt_widget.gantt.config.end_date = end && end.getValue() ? new Date(new Date(end.getValue()).valueOf()+86400000) : gantt_widget.gantt.getState().max_date;
|
|
if(gantt_widget.gantt.config.end_date <= gantt_widget.gantt.config.start_date)
|
|
{
|
|
gantt_widget.gantt.config.end_date = null;
|
|
if(end) end.set_value(null);
|
|
}
|
|
gantt_widget.set_zoom();
|
|
gantt_widget.gantt.render();
|
|
}
|
|
|
|
gantt_widget.gantt.refreshData();
|
|
}
|
|
// In case this gets bound twice, it's important to return
|
|
return true;
|
|
};
|
|
|
|
if(_widget.change != change) _widget.change = change;
|
|
}, this, et2_inputWidget);
|
|
},
|
|
|
|
/**
|
|
* Start UI for selecting among defined columns
|
|
*/
|
|
_column_selection: function(e)
|
|
{
|
|
var self = this;
|
|
var columns = [];
|
|
var columns_selected = [];
|
|
for (var i = 0; i < this.gantt_config.columns.length; i++)
|
|
{
|
|
var col = this.gantt_config.columns[i];
|
|
columns.push({
|
|
value: col.name,
|
|
label: col.label
|
|
})
|
|
if(!col.hide)
|
|
{
|
|
columns_selected.push(col.name);
|
|
}
|
|
}
|
|
|
|
// Build the popup
|
|
if(!this.selectPopup)
|
|
{
|
|
var select = et2_createWidget("select", {
|
|
multiple: true,
|
|
rows: 8,
|
|
empty_label:this.egw().lang("select columns"),
|
|
selected_first: false
|
|
}, this);
|
|
select.set_select_options(columns);
|
|
select.set_value(columns_selected);
|
|
|
|
var okButton = et2_createWidget("buttononly", {}, this);
|
|
okButton.set_label(this.egw().lang("ok"));
|
|
okButton.onclick = function() {
|
|
// Update columns
|
|
var value = select.getValue() || [];
|
|
for (var i = 0; i < columns.length; i++)
|
|
{
|
|
self.gantt_config.columns[i].hide = value.indexOf(columns[i].value) < 0 ;
|
|
}
|
|
self.set_columns(self.gantt_config.columns);
|
|
|
|
// Update Implicit preference
|
|
this.egw().set_preference(self.getInstanceManager().app, 'gantt_columns_' + self.id, value);
|
|
|
|
// Hide popup
|
|
self.selectPopup.toggle();
|
|
self.selectPopup.remove();
|
|
self.selectPopup = null;
|
|
$j('body').off('click.gantt');
|
|
};
|
|
|
|
var cancelButton = et2_createWidget("buttononly", {}, this);
|
|
cancelButton.set_label(this.egw().lang("cancel"));
|
|
cancelButton.onclick = function() {
|
|
self.selectPopup.toggle();
|
|
self.selectPopup.remove();
|
|
self.selectPopup = null;
|
|
$j('body').off('click.gantt');
|
|
};
|
|
|
|
// Create and add popup
|
|
this.selectPopup = jQuery(document.createElement("div"))
|
|
.addClass("colselection ui-dialog ui-widget-content")
|
|
.append(select.getDOMNode())
|
|
.append(okButton.getDOMNode())
|
|
.append(cancelButton.getDOMNode())
|
|
.appendTo(this.getInstanceManager().DOMContainer);
|
|
// Bind so if you click elsewhere, it closes
|
|
window.setTimeout(function() {$j(document).one('mouseup.gantt', function(e){
|
|
if(!self.selectPopup.is(e.target) && self.selectPopup.has(e.target).length === 0)
|
|
{
|
|
cancelButton.onclick();
|
|
}
|
|
});},1);
|
|
}
|
|
else
|
|
{
|
|
this.selectPopup.toggle();
|
|
}
|
|
this.selectPopup.position({my:'right top', at:'right bottom', of: e.target});
|
|
},
|
|
|
|
/**
|
|
* Link the actions to the DOM nodes / widget bits.
|
|
* Overridden to make the gantt chart a container, so it can't be selected.
|
|
* Because the chart handles its own AJAX fetching and parsing, for this widget
|
|
* we're trying dynamic binding as needed, rather than binding every single task
|
|
*
|
|
* @param {object} actions {ID: {attributes..}+} map of egw action information
|
|
*/
|
|
_link_actions: function(actions)
|
|
{
|
|
|
|
this._super.apply(this, arguments);
|
|
|
|
// Submit with most actions
|
|
this._actionManager.setDefaultExecute(jQuery.proxy(function(action, selected) {
|
|
var ids = [];
|
|
for(var i = 0; i < selected.length; i++)
|
|
{
|
|
ids.push(selected[i].id);
|
|
}
|
|
this.value = {
|
|
action: action.id,
|
|
selected: ids
|
|
};
|
|
|
|
// downloads need a regular submit via POST (no Ajax)
|
|
if (action.data.postSubmit)
|
|
{
|
|
this.getInstanceManager().postSubmit();
|
|
}
|
|
else
|
|
{
|
|
this.getInstanceManager().submit();
|
|
}
|
|
}, this));
|
|
|
|
// Get the top level element for the tree
|
|
var objectManager = egw_getAppObjectManager(true);
|
|
var widget_object = objectManager.getObjectById(this.id);
|
|
widget_object.flags = EGW_AO_FLAG_IS_CONTAINER;
|
|
},
|
|
|
|
/**
|
|
* Bind a single task as needed to the action system. This is instead of binding
|
|
* every single task at the start.
|
|
*
|
|
* @param {string} taskId
|
|
*/
|
|
_link_task: function(taskId)
|
|
{
|
|
if(!taskId) return;
|
|
var objectManager = egw_getObjectManager(this.id,false);
|
|
var obj = null;
|
|
if(!(obj = objectManager.getObjectById(taskId)))
|
|
{
|
|
obj = objectManager.addObject(taskId, this.dhtmlxGanttItemAOI(this.gantt,taskId));
|
|
obj.data = this.gantt.getTask(taskId);
|
|
obj.updateActionLinks(objectManager.actionLinks)
|
|
}
|
|
objectManager.setAllSelected(false);
|
|
obj.setSelected(true);
|
|
objectManager.updateSelectedChildren(obj,true)
|
|
},
|
|
|
|
/**
|
|
* ActionObjectInterface for gantt chart
|
|
*/
|
|
dhtmlxGanttItemAOI: function(gantt, task_id)
|
|
{
|
|
var aoi = new egwActionObjectInterface();
|
|
|
|
// Retrieve the actual node from the chart
|
|
aoi.node = gantt.getTaskNode(task_id);
|
|
aoi.id = task_id;
|
|
aoi.doGetDOMNode = function() {
|
|
return aoi.node;
|
|
}
|
|
|
|
aoi.doTriggerEvent = function(_event) {
|
|
if (_event == EGW_AI_DRAG_OVER)
|
|
{
|
|
$j(this.node).addClass("draggedOver");
|
|
}
|
|
if (_event == EGW_AI_DRAG_OUT)
|
|
{
|
|
$j(this.node).removeClass("draggedOver");
|
|
}
|
|
}
|
|
|
|
aoi.doSetState = function(_state) {
|
|
if(!gantt || !gantt.isTaskExists(this.id)) return;
|
|
|
|
if(egwBitIsSet(_state, EGW_AO_STATE_SELECTED))
|
|
{
|
|
gantt.selectTask(this.id); // false = do not trigger onSelect
|
|
}
|
|
else
|
|
{
|
|
gantt.unselectTask(this.id);
|
|
}
|
|
}
|
|
|
|
return aoi;
|
|
}
|
|
|
|
});
|
|
et2_register_widget(et2_gantt, ["gantt"]);
|
|
|
|
/**
|
|
* Common look, feel & settings for all Gantt charts
|
|
*/
|
|
// Localize to user's language - breaks if file is not there
|
|
//egw.includeJS("/phpgwapi/js/dhtmlxGantt/codebase/locale/locale_" + egw.preference('lang') + ".js");
|
|
|
|
$j(function() {
|
|
// Set icon to match application
|
|
gantt.templates.grid_file = function(item) {
|
|
if(!item.pe_icon || !egw.image(item.pe_icon)) return "<div class='gantt_tree_icon gantt_file'></div>";
|
|
return "<div class='gantt_tree_icon' style='background-image: url(\"" + egw.image(item.pe_icon) + "\");'/></div>";
|
|
}
|
|
|
|
// CSS for scale row, turns on clickable
|
|
gantt.templates.scale_row_class = function(scale) {
|
|
if(scale.unit != 'minute' && scale.unit != 'month')
|
|
{
|
|
return scale.class || 'et2_clickable';
|
|
}
|
|
return scale.class;
|
|
}
|
|
|
|
// Include progress text in the bar
|
|
gantt.templates.progress_text = function(start, end, task) {
|
|
return "<span>"+Math.round(task.progress*100)+ "% </span>";
|
|
};
|
|
|
|
// Highlight weekends
|
|
gantt.templates.scale_cell_class = function(date){
|
|
if(date.getDay()==0||date.getDay()==6){
|
|
return "weekend";
|
|
}
|
|
};
|
|
gantt.templates.task_cell_class = function(item,date){
|
|
if(date.getDay()==0||date.getDay()==6){
|
|
return "weekend"
|
|
}
|
|
};
|
|
|
|
gantt.templates.leftside_text = function(start, end, task) {
|
|
var text = '';
|
|
if(task.planned_start)
|
|
{
|
|
if(typeof task.planned_start == 'string') task.planned_start = gantt.date.parseDate(task.planned_start, "xml_date");
|
|
var p_start = gantt.posFromDate(task.planned_start) - gantt.posFromDate(start);
|
|
text = "<div class='gantt_task_line gantt_task_planned' style='width:"+Math.abs(p_start)+"px; right:"+(p_start > 0 ? -p_start : 0)+"px;'><span>"
|
|
+ gantt.date.date_to_str(gantt.config.api_date)(task.planned_start)
|
|
+ "</span></div>";
|
|
}
|
|
return text;
|
|
};
|
|
gantt.templates.rightside_text = function(start, end, task) {
|
|
var text = '';
|
|
if(task.planned_end)
|
|
{
|
|
if(typeof task.planned_end == 'string') task.planned_end = gantt.date.parseDate(task.planned_end, "xml_date");
|
|
var p_end = gantt.posFromDate(task.planned_end) - gantt.posFromDate(end);
|
|
text = "<div class='gantt_task_line gantt_task_planned' style='left:"+(p_end > 0 ? 0 : p_end) +"px; width:"+Math.abs(p_end)+"px'><span>"
|
|
+ gantt.date.date_to_str(gantt.config.api_date)(task.planned_end)
|
|
+ "</span></div>";
|
|
}
|
|
return text;
|
|
};
|
|
|
|
// Link styling
|
|
gantt.templates.link_class = function(link) {
|
|
var link_class = '';
|
|
var source = gantt.getTask(link.source);
|
|
var target = gantt.getTask(link.target);
|
|
var valid = true;
|
|
|
|
var types = gantt.config.links;
|
|
switch (link.type)
|
|
{
|
|
case types.finish_to_start:
|
|
valid = (source.end_date <= target.start_date)
|
|
break;
|
|
case types.start_to_start:
|
|
valid = (source.start_date <= target.start_date);
|
|
break;
|
|
case types.finish_to_finish:
|
|
valid = (source.end_date >= target.end_date);
|
|
break;
|
|
case types.start_to_finish:
|
|
valid = (source.start_date >= target.end_date)
|
|
break;
|
|
}
|
|
|
|
link_class += valid ? '' : 'invalid_constraint';
|
|
|
|
return link_class;
|
|
}
|
|
}); |