2014-04-16 21:47:29 +02:00
/ * *
* 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" ;
/ * e g w : u s e s
jsapi . jsapi ;
jquery . jquery ;
/ p h p g w a p i / j s / d h t m l x t r e e / j s / d h t m l X C o m m o n . j s ; / / o t h e r w i s e g a n t t b r e a k s
/ p h p g w a p i / j s / d h t m l x G a n t t / c o d e b a s e / d h t m l x g a n t t . j s ;
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
* /
2014-04-24 00:18:05 +02:00
var et2 _gantt = et2 _valueWidget . extend ( [ et2 _IResizeable ] ,
2014-04-16 21:47:29 +02:00
{
// 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"
} ,
2014-04-24 00:18:05 +02:00
"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."
} ,
2014-04-16 21:47:29 +02:00
value : { type : 'any' }
} ,
// 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' ,
show _progress : true ,
2014-05-07 16:41:15 +02:00
order _branch : true ,
2014-04-16 21:47:29 +02:00
min _column _width : 30 ,
fit _tasks : true ,
autosize : 'y' ,
2014-04-29 01:05:26 +02:00
// 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 : true ,
2014-04-16 21:47:29 +02:00
scale _unit : 'day' ,
2014-04-29 01:05:26 +02:00
date _scale : '%d' ,
2014-04-16 21:47:29 +02:00
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 ) ;
2014-04-24 00:18:05 +02:00
2014-04-16 21:47:29 +02:00
// Gantt instance
this . gantt = null ;
2014-04-24 00:18:05 +02:00
// DOM Nodes
2014-04-16 21:47:29 +02:00
this . filters = $j ( document . createElement ( "div" ) )
. addClass ( 'et2_gantt_header' ) ;
2014-04-24 00:18:05 +02:00
this . gantt _node = $j ( '<div style="width:100%;height:100%"></div>' ) ;
2014-04-16 21:47:29 +02:00
this . htmlNode = $j ( document . createElement ( "div" ) )
. css ( 'height' , this . options . height )
. addClass ( 'et2_gantt' ) ;
2014-04-24 00:18:05 +02:00
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
) ;
2014-04-16 21:47:29 +02:00
this . setDOMNode ( this . htmlNode [ 0 ] ) ;
} ,
destroy : function ( ) {
if ( this . gantt !== null )
{
this . gantt . detachAllEvents ( ) ;
this . gantt . clearAll ( ) ;
this . gantt = null ;
2014-04-24 00:18:05 +02:00
// Destroy dynamic full-height
if ( this . dynheight ) this . dynheight . free ( ) ;
2014-04-16 21:47:29 +02:00
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 ;
}
// Initialize chart
2014-04-24 00:18:05 +02:00
this . gantt = this . gantt _node . dhx _gantt ( config ) ;
2014-04-16 21:47:29 +02:00
if ( this . options . value )
{
this . set _value ( this . options . value ) ;
}
// Update start & end dates with chart values for consistency
if ( start _date )
{
start _date . set _value ( this . gantt . getState ( ) . min _date ) ;
}
if ( end _date )
{
end _date . set _value ( this . gantt . getState ( ) . max _date ) ;
}
// Bind some events to make things nice and et2
this . _bindGanttEvents ( ) ;
2014-04-24 00:18:05 +02:00
// Bind filters
2014-04-16 21:47:29 +02:00
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 ) ;
} ,
2014-04-24 00:18:05 +02:00
/ * *
* Implement the et2 _IResizable interface to resize
* /
resize : function ( )
{
if ( this . dynheight )
{
this . dynheight . update ( function ( w , h ) {
this . gantt . setSizes ( ) ;
} , this ) ;
}
else
{
this . gantt . setSizes ( ) ;
}
} ,
2014-04-16 21:47:29 +02:00
/ * *
* 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" }
* ]
* } ;
* 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 ;
// Ensure proper format, no extras
var safe _value = {
data : value . data || [ ] ,
links : value . links || [ ]
} ;
this . gantt . parse ( safe _value ) ;
// Set some things from the value
// Set zoom
if ( ! this . options . zoom ) this . set _zoom ( ) ;
// If this is not the first gantt chart the browser renders, sometimes it needs a nudge
try
{
this . gantt . render ( ) ;
}
catch ( e )
{
this . egw ( ) . debug ( 'warning' , 'Problem rendering gantt' , e ) ;
}
} ,
/ * *
* 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 ;
// No level? Auto calculate.
if ( level > 4 ) level = 4 ;
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 . render ( ) ;
}
catch ( e )
{ }
var difference = ( this . gantt . getState ( ) . max _date - this . gantt . getState ( ) . min _date ) / 1000 ; // seconds
// Spans more than a year
if ( difference > 31536000 || this . gantt . getState ( ) . max _date . getFullYear ( ) != this . gantt . getState ( ) . min _date . getFullYear ( ) )
{
level = 4 ;
}
// More than 2 months
else if ( difference > 5256000 || this . gantt . getState ( ) . max _date . getMonth ( ) != this . gantt . getState ( ) . min _date . getMonth ( ) )
{
level = 3 ;
}
// More than 3 days
else if ( difference > 259200 )
{
level = 2 ;
}
else
{
level = 1 ;
}
}
// Adjust Gantt settings for specified level
switch ( level )
{
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
2014-04-24 00:18:05 +02:00
subscales . push ( { unit : "month" , step : 1 , date : '%F %Y' } ) ;
2014-04-16 21:47:29 +02:00
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" ;
2014-04-24 00:18:05 +02:00
step = parseInt ( this . egw ( ) . preference ( 'interval' , 'calendar' ) || 15 ) ;
scale _unit = 'minute' ;
2014-04-16 21:47:29 +02:00
}
// 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 ;
2014-04-24 00:18:05 +02:00
this . options . zoom = level ;
2014-04-16 21:47:29 +02:00
return level ;
} ,
/ * *
* Bind all the internal gantt events for nice widget actions
* /
_bindGanttEvents : function ( ) {
var gantt _widget = this ;
2014-04-24 00:18:05 +02:00
// Click on scale to zoom - top zooms out, bottom zooms in
this . gantt _node . on ( 'click' , '.gantt_scale_line' , function ( e ) {
if ( this . parentNode . firstChild == this )
{
// 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 ( ) ;
}
} ) ;
2014-05-07 16:41:15 +02:00
this . gantt . attachEvent ( "onContextMenu" , function ( taskId , linkId , e ) {
gantt _widget . _link _task ( taskId ) ;
return false ;
} )
2014-04-16 21:47:29 +02:00
// Double click
this . gantt . attachEvent ( "onBeforeLightbox" , function ( id ) {
2014-05-07 16:41:15 +02:00
gantt _widget . _link _task ( id ) ;
// Don't do gantt default actions, actions handle it
2014-04-16 21:47:29 +02:00
return false ;
} ) ;
2014-04-24 00:18:05 +02:00
// Update server after dragging a task
this . gantt . attachEvent ( "onAfterTaskDrag" , function ( id , mode , e ) {
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 ) ;
if ( gantt _widget . options . ajax _update )
{
var request = gantt _widget . egw ( ) . json ( gantt _widget . options . ajax _update ,
[ task , value ]
) . sendRequest ( true ) ;
}
} ) ;
2014-04-16 21:47:29 +02:00
// Bind AJAX for dynamic expansion
2014-04-24 00:18:05 +02:00
// TODO: This could be improved
2014-04-16 21:47:29 +02:00
this . gantt . attachEvent ( "onTaskOpened" , function ( id , item ) {
// Node children are already there & displayed
2014-04-24 00:18:05 +02:00
var value = gantt _widget . getInstanceManager ( ) . getValues ( gantt _widget . getInstanceManager ( ) . widgetContainer ) ;
var request = gantt _widget . egw ( ) . json ( gantt _widget . options . autoload ,
[ id , value ] ,
function ( data ) {
this . parse ( data ) ;
} ,
this , true , this
) . sendRequest ( ) ;
2014-04-16 21:47:29 +02:00
} ) ;
// 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 'start_date' :
if ( _widget . getValue ( ) )
{
display = display && ( ( task [ _widget . id ] . valueOf ( ) / 1000 ) >= _widget . getValue ( ) ) ;
}
return ;
case 'end_date' :
// End date is not actually a required field, so accept undefined too
if ( _widget . getValue ( ) )
{
display = display && ( typeof task [ _widget . id ] == 'undefined' || ! task [ _widget . id ] || ( ( task [ _widget . id ] . valueOf ( ) / 1000 ) <= _widget . getValue ( ) ) ) ;
}
return ;
}
// Regular equality comparison
2014-04-24 00:18:05 +02:00
if ( _widget . getValue ( ) && typeof task [ _widget . id ] != 'undefined' )
2014-04-16 21:47:29 +02:00
{
2014-04-24 00:18:05 +02:00
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 ;
}
}
2014-04-16 21:47:29 +02:00
}
} , gantt _widget , et2 _inputWidget ) ;
return display ;
} ) ;
} ,
/ * *
* Bind onchange for any child input widgets
* /
_bindChildren : function ( ) {
var gantt _widget = this ;
this . iterateOver ( function ( _widget ) {
// 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 && _widget . isDirty ( ) ) {
// 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 ( ) * 1000 ) : gantt _widget . gantt . getState ( ) . min _date ;
gantt _widget . gantt . config . end _date = end && end . getValue ( ) ? new Date ( end . getValue ( ) * 1000 ) : 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 ) ;
2014-05-07 16:41:15 +02:00
} ,
/ * *
* 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 ) ;
// 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 )
{
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 ;
2014-04-16 21:47:29 +02:00
}
2014-05-07 16:41:15 +02:00
2014-04-16 21:47:29 +02:00
} ) ;
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");
// Set icon to match application
gantt . templates . grid _file = function ( item ) {
if ( ! item . pe _app || ! 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>" ;
}
// Show nicer intervals in minute duration
gantt . templates . date _scale = function ( date ) {
if ( gantt . config . scale _unit == 'minute' )
{
date . setMinutes ( ( date . getMinutes ( ) % this . gantt . config . step ) * this . gantt . config . step ) ;
}
return gantt . date . date _to _str ( gantt . config . date _scale ) ( date ) ;
}