2011-09-09 13:29:07 +02:00
< ? php
/**
* EGroupware - eTemplate serverside implementation of the nextmatch widget
*
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package etemplate
* @ subpackage api
* @ link http :// www . egroupware . org
* @ author Ralf Becker < RalfBecker @ outdoor - training . de >
* @ copyright 2002 - 11 by RalfBecker @ outdoor - training . de
2011-09-09 16:00:30 +02:00
* @ version $Id $
2011-09-09 13:29:07 +02:00
*/
/**
2011-09-09 16:00:30 +02:00
* eTemplate serverside implementation of the nextmatch widget
*
* $content [ $id ] = array ( // I = value set by the app, 0 = value on return / output
* 'get_rows' => // I method/callback to request the data for the rows eg. 'notes.bo.get_rows'
* 'filter_label' => // I label for filter (optional)
* 'filter_help' => // I help-msg for filter (optional)
* 'no_filter' => True // I disable the 1. filter
* 'no_filter2' => True // I disable the 2. filter (params are the same as for filter)
* 'no_cat' => True // I disable the cat-selectbox
* 'cat_app' => // I application the cat's should be from, default app in get_rows
* 'cat_is_select' => // I true||'no_lang' use selectbox instead of category selection, default null
* 'template' => // I template to use for the rows, if not set via options
* 'header_left' => // I template to show left of the range-value, left-aligned (optional)
* 'header_right' => // I template to show right of the range-value, right-aligned (optional)
* 'bottom_too' => True // I show the nextmatch-line (arrows, filters, search, ...) again after the rows
* 'never_hide' => True // I never hide the nextmatch-line if less then maxmatch entries
* 'lettersearch' => True // I show a lettersearch
* 'searchletter' => // I0 active letter of the lettersearch or false for [all]
* 'start' => // IO position in list
* 'num_rows' => // IO number of rows to show, defaults to maxmatches from the general prefs
* 'cat_id' => // IO category, if not 'no_cat' => True
* 'search' => // IO search pattern
* 'order' => // IO name of the column to sort after (optional for the sortheaders)
* 'sort' => // IO direction of the sort: 'ASC' or 'DESC'
* 'col_filter' => // IO array of column-name value pairs (optional for the filterheaders)
* 'filter' => // IO filter, if not 'no_filter' => True
* 'filter_no_lang' => True // I set no_lang for filter (=dont translate the options)
* 'filter_onchange' => 'this.form.submit();' // I onChange action for filter, default: this.form.submit();
* 'filter2' => // IO filter2, if not 'no_filter2' => True
* 'filter2_no_lang' => True // I set no_lang for filter2 (=dont translate the options)
* 'filter2_onchange' => 'this.form.submit();' // I onChange action for filter2, default: this.form.submit();
* 'rows' => // O content set by callback
* 'total' => // O the total number of entries
* 'sel_options' => // O additional or changed sel_options set by the callback and merged into $tmpl->sel_options
* 'no_columnselection' => // I turns off the columnselection completly, turned on by default
* 'columnselection-pref' => // I name of the preference (plus 'nextmatch-' prefix), default = template-name
* 'default_cols' => // I columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns
* 'options-selectcols' => // I array with name/label pairs for the column-selection, this gets autodetected by default. A name => false suppresses a column completly.
* 'return' => // IO allows to return something from the get_rows function if $query is a var-param!
* 'csv_fields' => // I false=disable csv export, true or unset=enable it with auto-detected fieldnames or preferred importexport definition,
* array with name => label or name => array ( 'label' => label , 'type' => type ) pairs ( type is a eT widget - type )
* or name of import / export definition
* 'row_id' => // I key into row content to set it's value as tr id, eg. 'id'
* 'actions' => // I array with actions, see nextmatch_widget::egw_actions
* 'action_links' => // I array with enabled actions or ones which should be checked if they are enabled
* optional , default id of all first level actions plus the ones with enabled = 'javaScript:...'
* 'action_var' => 'action' // I name of var to return choosen action, default 'action'
* 'action' => // O string selected action
* 'selected' => // O array with selected id's
* 'checkboxes' => // O array with checkbox id as key and boolean checked value
* 'select_all' => // O boolean value of select_all checkbox, reference to above value for key 'select_all'
2011-09-09 13:29:07 +02:00
*/
class etemplate_widget_nextmatch extends etemplate_widget
{
2011-09-09 16:00:30 +02:00
public function __construct ( $xml = '' )
{
2011-09-09 16:32:55 +02:00
if ( $xml ) {
parent :: __construct ( $xml );
// TODO: probably a better way to do this
egw_framework :: includeCSS ( '/phpgwapi/js/egw_action/test/skins/dhtmlxmenu_egw.css' );
}
2011-09-09 13:29:07 +02:00
}
2011-10-05 18:12:40 +02:00
/**
* Legacy options
*/
protected $legacy_options = 'template' ;
2012-03-20 15:25:12 +01:00
2011-09-09 16:00:30 +02:00
/**
* Number of rows to send initially
*/
const INITIAL_ROWS = 25 ;
/**
* Set up what we know on the server side .
*
* Sending a first chunk of rows
*
* @ param string $cname
2012-05-03 20:06:27 +02:00
* @ param array $expand values for keys 'c' , 'row' , 'c_' , 'row_' , 'cont'
2011-09-09 16:00:30 +02:00
*/
2012-05-03 20:06:27 +02:00
public function beforeSendToClient ( $cname , array $expand )
2011-09-09 16:00:30 +02:00
{
$attrs = $this -> attrs ;
2012-05-03 20:06:27 +02:00
$form_name = self :: form_name ( $cname , $this -> id , $expand );
2012-07-17 01:00:44 +02:00
$value = self :: get_array ( self :: $request -> content , $form_name , true );
2011-09-09 16:00:30 +02:00
$value [ 'start' ] = 0 ;
$value [ 'num_rows' ] = self :: INITIAL_ROWS ;
2011-09-14 11:41:08 +02:00
$value [ 'rows' ] = array ();
2012-07-17 01:00:44 +02:00
$send_value = $value ;
$total = self :: call_get_rows ( $send_value , $send_value [ 'rows' ], self :: $request -> readonlys );
$value =& self :: get_array ( self :: $request -> content , $form_name , true );
$value = $send_value ;
$value [ 'total' ] = $total ;
2011-10-03 16:56:20 +02:00
// Send categories
if ( ! $value [ 'no_cat' ] && ! $value [ 'cat_is_select' ])
{
2011-10-03 18:02:41 +02:00
$cat_app = $value [ 'cat_app' ] ? $value [ 'cat_app' ] : $GLOBALS [ 'egw_info' ][ 'flags' ][ 'current_app' ];
$value [ 'options-cat_id' ] = array ( '' => lang ( 'all' )) + etemplate_widget_menupopup :: typeOptions ( 'select-cat' , ',,' . $cat_app , $no_lang , false , $value [ 'cat_id' ]);
2012-03-16 15:52:33 +01:00
// Prevent double encoding - widget does this on its own, but we're just grabbing the options
foreach ( $value [ 'options-cat_id' ] as & $label )
{
if ( ! is_array ( $label ))
{
$label = html_entity_decode ( $label , ENT_NOQUOTES , 'utf-8' );
}
elseif ( $label [ 'label' ])
{
$label [ 'label' ] = html_entity_decode ( $label [ 'label' ], ENT_NOQUOTES , 'utf-8' );
}
}
2011-10-03 16:56:20 +02:00
}
2012-05-24 23:30:19 +02:00
foreach ( $value as $name => $_value )
{
if ( strpos ( $name , 'options-' ) !== false )
{
$select = substr ( $name , 8 );
self :: $request -> sel_options [ $select ] = $_value ;
2012-05-30 18:34:20 +02:00
// The client doesn't need them in content, but we can't unset them because
// some apps don't send them on re-load, pulling them from the session
//unset($value[$name]);
2012-05-24 23:30:19 +02:00
}
}
2012-07-17 01:00:44 +02:00
if ( $value [ 'rows' ][ 'sel_options' ])
{
self :: $request -> sel_options = array_merge ( self :: $request -> sel_options , $value [ 'rows' ][ 'sel_options' ]);
unset ( $value [ 'rows' ][ 'sel_options' ]);
}
2012-05-24 23:30:19 +02:00
2011-09-09 16:00:30 +02:00
// todo: no need to store rows in request, it's enought to send them to client
2012-03-20 15:25:12 +01:00
//error_log(__METHOD__."() $this: total=$value[total]");
2011-09-14 11:41:08 +02:00
//foreach($value['rows'] as $n => $row) error_log("$n: ".array2string($row));
// set up actions, but only if they are defined AND not already set up (run throught self::egw_actions())
if ( isset ( $value [ 'actions' ]) && ! isset ( $value [ 'actions' ][ 0 ]))
{
2012-03-27 16:02:34 +02:00
$value [ 'action_links' ] = array ();
2011-09-14 11:41:08 +02:00
$template_name = isset ( $value [ 'template' ]) ? $value [ 'template' ] : $this -> attrs [ 'options' ];
if ( ! is_array ( $value [ 'action_links' ])) $value [ 'action_links' ] = array ();
$value [ 'actions' ] = self :: egw_actions ( $value [ 'actions' ], $template_name , '' , $value [ 'action_links' ]);
}
2011-09-09 16:00:30 +02:00
}
/**
* Callback to fetch more rows
*
2012-03-20 15:25:12 +01:00
* Callback uses existing get_rows callback , but requires now 'row_id' to be set .
* If no 'row_modified' is set , rows cant checked for modification and therefore
* are always returned to client if in range or deleted if outside range .
*
2011-09-09 16:00:30 +02:00
* @ param string $exec_id identifys the etemplate request
2012-03-30 14:28:07 +02:00
* @ param array $queriedRange array with values for keys " start " , " num_rows " and optional " refresh " , " parent_id "
2011-09-27 19:58:10 +02:00
* @ param array $filters Search and filter parameters , passed to data source
2012-03-20 15:25:12 +01:00
* @ param string $form_name = 'nm' full id of widget incl . all namespaces
* @ param array $knownUids = null uid ' s know to client
* @ param int $lastModified = null date $knowUids last checked
* @ todo for $queriedRange [ refresh ] first check if there ' s any modification since $lastModified , return $result [ order ] === null
* @ return array with values for keys 'total' , 'rows' , 'readonlys' , 'order' , 'data' and 'lastModification'
2011-09-09 16:00:30 +02:00
*/
2012-03-20 15:25:12 +01:00
static public function ajax_get_rows ( $exec_id , array $queriedRange , array $filters = array (), $form_name = 'nm' ,
array $knownUids = null , $lastModified = null )
2011-09-09 13:29:07 +02:00
{
2011-09-09 16:00:30 +02:00
self :: $request = etemplate_request :: read ( $exec_id );
$value = self :: get_array ( self :: $request -> content , $form_name , true );
2012-06-18 19:43:39 +02:00
if ( ! is_array ( $value ))
{
$value = ( $value ) ? array ( $value ) : array ();
}
2011-09-27 19:58:10 +02:00
$value = array_merge ( $value , $filters );
2012-05-14 18:46:52 +02:00
//error_log(__METHOD__."('".substr($exec_id,0,10)."...', range=".array2string($queriedRange).', filters='.array2string($filters).", '$form_name', knownUids=".array2string($knownUids).", lastModified=$lastModified) parent_id=$value[parent_id], is_parent=$value[is_parent]");
2012-03-30 14:28:07 +02:00
2012-03-20 15:25:12 +01:00
$result = array ();
2011-09-09 13:29:07 +02:00
2011-10-19 00:20:27 +02:00
// Parse sort into something that get_rows functions are expecting: db_field in order, ASC/DESC in sort
2011-10-03 17:34:10 +02:00
if ( is_array ( $value [ 'sort' ]))
2011-10-19 00:20:27 +02:00
{
$value [ 'order' ] = $value [ 'sort' ][ 'id' ];
2012-03-20 15:25:12 +01:00
$value [ 'sort' ] = $value [ 'sort' ][ 'asc' ] ? 'ASC' : 'DESC' ;
2011-10-19 00:20:27 +02:00
}
2011-10-03 17:34:10 +02:00
2012-03-20 15:25:12 +01:00
$value [ 'start' ] = ( int ) $queriedRange [ 'start' ];
$value [ 'num_rows' ] = ( int ) $queriedRange [ 'num_rows' ];
2012-04-18 00:56:04 +02:00
if ( $value [ 'num_rows' ] == 0 ) $value [ 'num_rows' ] = 20 ;
2012-03-30 14:28:07 +02:00
// if app supports parent_id / hierarchy ($value['parent_id'] not empty), set parent_id as filter
if (( $parent_id = $value [ 'parent_id' ]))
{
2012-07-11 18:01:14 +02:00
// Infolog at least wants 'parent_id' instead of $parent_id
$value [ 'col_filter' ][ 'parent_id' ] = $queriedRange [ 'parent_id' ];
2012-03-30 14:28:07 +02:00
}
2012-07-05 00:41:51 +02:00
// If specific data requested, just do that
2012-07-11 18:01:14 +02:00
if (( $row_id = $value [ 'row_id' ]) && $queriedRange [ 'refresh' ])
2012-07-05 00:41:51 +02:00
{
$value [ 'col_filter' ][ $row_id ] = $queriedRange [ 'refresh' ];
}
2012-03-20 15:25:12 +01:00
$rows = $result [ 'data' ] = $result [ 'order' ] = array ();
$result [ 'total' ] = self :: call_get_rows ( $value , $rows , $result [ 'readonlys' ]);
$result [ 'lastModification' ] = egw_time :: to ( 'now' , 'ts' ) - 1 ;
$row_id = isset ( $value [ 'row_id' ]) ? $value [ 'row_id' ] : 'id' ;
2012-03-30 14:28:07 +02:00
$row_modified = $value [ 'row_modified' ];
$is_parent = $value [ 'is_parent' ];
2012-03-23 15:42:06 +01:00
$kUkey = false ;
2012-03-20 15:25:12 +01:00
foreach ( $rows as $n => $row )
2011-09-09 16:00:30 +02:00
{
2012-03-20 15:25:12 +01:00
if ( is_int ( $n ))
{
if ( ! isset ( $row [ $row_id ])) unset ( $row_id ); // unset default row_id of 'id', if not used
2012-03-23 17:05:06 +01:00
if ( ! isset ( $row [ $row_modified ])) unset ( $row_modified );
2011-09-09 16:00:30 +02:00
2012-03-20 15:25:12 +01:00
$id = $row_id ? $row [ $row_id ] : $n ;
2012-03-23 15:42:06 +01:00
$result [ 'order' ][] = $id ;
2011-09-09 16:32:55 +02:00
2012-03-20 15:25:12 +01:00
// check if we need to send the data
if ( ! $row_id || ! $knownUids || ( $kUkey = array_search ( $id , $knownUids )) === false ||
! $lastModified || ! isset ( $row [ $row_modified ]) || $row [ $row_modified ] > $lastModified )
{
2012-03-30 14:28:07 +02:00
if ( $parent_id ) // if app supports parent_id / hierarchy, set parent_id and is_parent
{
$row [ 'is_parent' ] = $row [ $is_parent ];
$row [ 'parent_id' ] = $row [ $parent_id ];
}
2012-03-20 15:25:12 +01:00
$result [ 'data' ][ $id ] = $row ;
}
2012-03-23 15:42:06 +01:00
if ( $kUkey !== false ) unset ( $knownUids [ $kUkey ]);
2012-03-20 15:25:12 +01:00
}
else // non-row data set by get_rows method
{
$result [ 'rows' ][ $n ] = $row ;
}
}
2012-03-23 17:05:06 +01:00
// check knowUids outside of range for modification
2012-03-20 15:25:12 +01:00
if ( $knownUids )
{
2012-03-23 17:05:06 +01:00
if ( ! $row_id ) // row_id not set by nextmatch user --> tell client to delete data, as we cant identify rows
2012-03-20 15:25:12 +01:00
{
2012-03-23 17:05:06 +01:00
foreach ( $knownUids as $uid )
2012-03-20 15:25:12 +01:00
{
2012-03-23 17:05:06 +01:00
$result [ 'data' ][ $uid ] = null ;
2012-03-20 15:25:12 +01:00
}
}
2012-03-23 17:05:06 +01:00
else
2012-03-20 15:25:12 +01:00
{
2012-03-23 17:05:06 +01:00
//error_log(__METHOD__."() knowUids left to check ".array2string($knownUids));
// check if they are up to date: we create a query similar to csv-export without any filters
$value [ 'csv_export' ] = 'knownUids' ; // do not store $value in session
$value [ 'filter' ] = $value [ 'filter2' ] = $value [ 'cat_id' ] = $value [ 'search' ] = '' ;
$value [ 'col_filter' ] = array ( $row_id => $knownUids );
// if we know name of modification column and have a last-modified date
if ( $row_modified && $lastModified ) // --> set filter to return only modified entries
2012-03-23 15:42:06 +01:00
{
2012-03-23 17:05:06 +01:00
$value [ 'col_filter' ][] = $row_modified . ' > ' . ( int ) $lastModified ;
}
$value [ 'start' ] = 0 ;
$value [ 'num_rows' ] = count ( $knownUids );
$rows = array ();
if ( self :: call_get_rows ( $value , $rows ))
{
foreach ( $rows as $row )
{
if ( ! $row_modified || ! isset ( $row [ $row_modified ]) ||
! isset ( $lastModified ) || $row [ $row_modified ] > $lastModified )
{
$result [ 'data' ][ $row [ $row_id ]] = $row ;
}
}
2012-03-23 15:42:06 +01:00
}
2012-03-20 15:25:12 +01:00
}
}
2012-03-23 17:05:06 +01:00
foreach ( $result as $name => $value ) if ( $name != 'readonlys' ) error_log ( __METHOD__ . " () result[' $name ']= " . array2string ( $name == 'data' ? array_keys ( $value ) : $value ));
2011-09-14 11:41:08 +02:00
egw_json_response :: get () -> data ( $result );
2011-09-09 13:29:07 +02:00
}
2011-09-09 16:00:30 +02:00
/**
* Calling our callback
*
* Signature of get_rows callback is either :
* a ) int get_rows ( $query , & $rows , & $readonlys )
* b ) int get_rows ( & $query , & $rows , & $readonlys )
*
* If get_rows is called static ( and php >= 5.2 . 3 ), it is always b ) independent on how it ' s defined !
*
* @ param array & $value
2011-09-14 11:41:08 +02:00
* @ param array & $rows on return : rows are indexed by their row - number : $value [ start ], ... , $value [ start ] + $value [ num_rows ] - 1
2011-09-09 16:00:30 +02:00
* @ param array & $readonlys = null
* @ param object $obj = null ( internal )
* @ param string | array $method = null ( internal )
* @ return int | boolean total items found of false on error ( $value [ 'get_rows' ] not callable )
*/
2011-09-14 11:41:08 +02:00
private static function call_get_rows ( array & $value , array & $rows , array & $readonlys = null , $obj = null , $method = null )
2011-09-09 16:00:30 +02:00
{
if ( is_null ( $method )) $method = $value [ 'get_rows' ];
if ( is_null ( $obj ))
{
// allow static callbacks
if ( strpos ( $method , '::' ) !== false )
{
list ( $class , $method ) = explode ( '::' , $method );
// workaround for php < 5.2.3: do NOT call it static, but allow application code to specify static callbacks
if ( version_compare ( PHP_VERSION , '5.2.3' , '>=' ))
{
$method = array ( $class , $method );
unset ( $class );
}
}
else
{
list ( $app , $class , $method ) = explode ( '.' , $value [ 'get_rows' ]);
}
if ( $class )
{
if ( ! $app && ! is_object ( $GLOBALS [ $class ]))
{
$GLOBALS [ $class ] = new $class ();
}
if ( is_object ( $GLOBALS [ $class ])) // use existing instance (put there by a previous CreateObject)
{
$obj = $GLOBALS [ $class ];
}
else
{
$obj = CreateObject ( $app . '.' . $class );
}
}
}
2012-06-18 19:43:39 +02:00
if ( ! is_array ( $raw_rows )) $raw_rows = array ();
2011-09-14 11:41:08 +02:00
if ( ! is_array ( $readonlys )) $readonlys = array ();
2011-09-09 16:00:30 +02:00
if ( is_callable ( $method )) // php5.2.3+ static call (value is always a var param!)
{
2011-09-14 11:41:08 +02:00
$total = call_user_func_array ( $method , array ( & $value , & $raw_rows , & $readonlys ));
2011-09-09 16:00:30 +02:00
}
elseif ( is_object ( $obj ) && method_exists ( $obj , $method ))
{
2011-09-14 11:41:08 +02:00
$total = $obj -> $method ( $value , $raw_rows , $readonlys );
2011-09-09 16:00:30 +02:00
}
else
{
$total = false ; // method not callable
}
/* no automatic fallback to start = 0
if ( $method && $total && $value [ 'start' ] >= $total )
{
$value [ 'start' ] = 0 ;
$total = self :: call_get_rows ( $value , $rows , $readonlys , $obj , $method );
}
*/
2011-09-14 11:41:08 +02:00
// otherwise we might get stoped by max_excutiontime
2011-09-09 16:00:30 +02:00
if ( $total > 200 ) @ set_time_limit ( 0 );
2011-09-09 21:46:44 +02:00
// remove empty rows required by old etemplate to compensate for header rows
2012-05-14 18:46:52 +02:00
$first = $total ? null : 0 ;
2011-09-14 11:41:08 +02:00
foreach ( $raw_rows as $n => $row )
2011-09-09 21:46:44 +02:00
{
2011-09-14 11:41:08 +02:00
// skip empty rows inserted for each header-line in old etemplate
if ( is_int ( $n ) && is_array ( $rows ))
{
if ( is_null ( $first )) $first = $n ;
$rows [ $n - $first + $value [ 'start' ]] = $row ;
}
2012-07-17 01:00:44 +02:00
elseif ( ! is_numeric ( $n )) // rows with string-keys, after numeric rows
2011-09-14 11:41:08 +02:00
{
$rows [ $n ] = $row ;
}
2011-09-09 21:46:44 +02:00
}
2011-09-09 16:00:30 +02:00
//error_log($value['get_rows'].'() returning '.array2string($total).', method = '.array2string($method).', value = '.array2string($value));
return $total ;
}
2011-09-14 11:41:08 +02:00
/**
* Default maximum lenght for context submenus , longer menus are put as a " More " submenu
*/
const DEFAULT_MAX_MENU_LENGTH = 14 ;
/**
* Return egw_actions
*
* The following attributes are understood for actions on eTemplate / PHP side :
* - string 'id' id of the action ( set as key not attribute ! )
* - string 'caption' name / label or action , get ' s automatic translated
* - boolean 'no_lang' do NOT translate caption , default false
* - string 'icon' icon , eg . 'edit' or 'infolog/task' , if no app given app of template or API is used
* - string 'iconUrl' full url of icon , better use 'icon'
* - boolean | string 'allowOnMultiple' should action be shown if multiple lines are marked , or string 'only' , default true !
* - boolean | string 'enabled' is action available , or string with javascript function to call , default true !
* - string 'disableClass' class name to use with enabled = 'javaScript:nm_not_disableClass'
* ( add that css class in get_rows (), if row lacks rights for an action )
* - boolena 'hideOnDisabled' hide disabled actions , default false
* - string 'type' type of action , default 'popup' for contenxt menus , 'drag' or 'drop'
* - boolean 'default' is that action the default action , default false
* - array 'children' array with actions of submenu
* - int 'group' to group items , default all actions are in one group
* - string 'onExecute' javascript to run , default 'javaScript:nm_action' ,
* which runs action specified in nm_action attribute :
* - string 'nm_action'
* + 'alert' debug action , shows alert with action caption , id and id ' s of selected rows
* + 'submit' default action , sets nm [ action ], nm [ selected ] and nm [ select_all ]
* + 'location' redirects / set location . href to 'url' attribute
* + 'popup' opens popup with url given in 'url' attribute
* - string 'url' url for location or popup
* - string 'target' target for location or popup
* - string 'width' for popup
* - string 'height' for popup
* - string 'confirm' confirmation message
* - string 'confirm_multiple' confirmation message for multiple selected , defaults to 'confirm'
*
* That ' s what we should return looks JSON encoded like
* [
* {
* " id " : " folder_open " ,
* " iconUrl " : " imgs/folder.png " ,
* " caption " : " Open folder " ,
* " onExecute " : " javaScript:nm_action " ,
* " allowOnMultiple " : false ,
* " type " : " popup " ,
* " default " : true
* },
* ]
*
* @ param array $actions id indexed array of actions / array with valus for keys : 'iconUrl' , 'caption' , 'onExecute' , ...
* @ param string $template_name = '' name of the template , used as default for app name of images
* @ param string $prefix = '' prefix for ids
* @ param array & $action_links = array () on return all first - level actions plus the ones with enabled = 'javaScript:...'
* @ param int $max_length = self :: DEFAULT_MAX_MENU_LENGTH automatic pagination , not for first menu level !
* @ param array $default_attrs = null default attributes
* @ return array
*/
public static function egw_actions ( array $actions = null , $template_name = '' , $prefix = '' , array & $action_links = array (),
$max_length = self :: DEFAULT_MAX_MENU_LENGTH , array $default_attrs = null )
{
//echo "<p>".__METHOD__."(\$actions, '$template_name', '$prefix', \$action_links, $max_length) \$actions="; _debug_array($actions);
// default icons for some common actions
static $default_icons = array (
'view' => 'view' ,
'edit' => 'edit' ,
'open' => 'edit' , // does edit if possible, otherwise view
'add' => 'new' ,
'new' => 'new' ,
'delete' => 'delete' ,
'cat' => 'attach' , // add as category icon to api
'document' => 'etemplate/merge' ,
'print' => 'print' ,
'copy' => 'copy' ,
'move' => 'move' ,
'cut' => 'cut' ,
'paste' => 'editpaste' ,
);
$first_level = ! $action_links ; // add all first level actions
//echo "actions="; _debug_array($actions);
$egw_actions = array ();
$n = 1 ;
foreach (( array ) $actions as $id => $action )
{
// in case it's only selectbox id => label pairs
if ( ! is_array ( $action )) $action = array ( 'caption' => $action );
if ( $default_attrs ) $action += $default_attrs ;
if ( ! $first_level && $n == $max_length && count ( $actions ) > $max_length )
{
$id = 'more_' . count ( $actions ); // we need a new unique id
$action = array (
'caption' => 'More' ,
'prefix' => $prefix ,
// display rest of actions incl. current one as children
'children' => array_slice ( $actions , $max_length - 1 , count ( $actions ) - $max_length + 1 , true ),
);
//echo "*** Inserting id=$prefix$id"; _debug_array($action);
// we break at end of foreach loop, as rest of actions is already dealt with
// by putting them as children
}
$action [ 'id' ] = $prefix . $id ;
// set certain enable functions
foreach ( array (
'enableClass' => 'javaScript:nm_enableClass' ,
'disableClass' => 'javaScript:nm_not_disableClass' ,
'enableId' => 'javaScript:nm_enableId' ,
) as $attr => $check )
{
if ( isset ( $action [ $attr ]) && ! isset ( $action [ 'enabled' ]))
{
$action [ 'enabled' ] = $check ;
}
}
// add all first level popup actions plus ones with enabled = 'javaScript:...' to action_links
if (( ! isset ( $action [ 'type' ]) || in_array ( $action [ 'type' ], array ( 'popup' , 'drag' ))) && // popup is the default
( $first_level || substr ( $action [ 'enabled' ], 0 , 11 ) == 'javaScript:' ))
{
$action_links [] = $action [ 'id' ];
}
// set default icon, if no other is specified
if ( ! isset ( $action [ 'icon' ]) && isset ( $default_icons [ $id ]))
{
$action [ 'icon' ] = $default_icons [ $id ];
}
// use common eTemplate image semantics
if ( ! isset ( $action [ 'iconUrl' ]) && ! empty ( $action [ 'icon' ]))
{
list ( $app , $img ) = explode ( '/' , $action [ 'icon' ], 2 );
if ( ! $app || ! $img || ! is_dir ( EGW_SERVER_ROOT . '/' . $app ) || strpos ( $img , '/' ) !== false )
{
$img = $action [ 'icon' ];
list ( $app ) = explode ( '.' , $template_name );
}
$action [ 'iconUrl' ] = common :: find_image ( $app , $img );
unset ( $action [ 'icon' ]); // no need to submit it
}
// translate labels
if ( ! $action [ 'no_lang' ])
{
$action [ 'caption' ] = lang ( $action [ 'caption' ]);
if ( $action [ 'hint' ]) $action [ 'hint' ] = lang ( $action [ 'hint' ]);
}
unset ( $action [ 'no_lang' ]);
foreach ( array ( 'confirm' , 'confirm_multiple' ) as $confirm )
{
if ( isset ( $action [ $confirm ]))
{
$action [ $confirm ] = lang ( $action [ $confirm ]) . ( substr ( $action [ $confirm ], - 1 ) != '?' ? '?' : '' );
}
}
// add sub-menues
if ( $action [ 'children' ])
{
static $inherit_attrs = array ( 'url' , 'popup' , 'nm_action' , 'onExecute' , 'type' , 'egw_open' , 'allowOnMultiple' , 'confirm' , 'confirm_multiple' );
$action [ 'children' ] = self :: egw_actions ( $action [ 'children' ], $template_name , $action [ 'prefix' ], $action_links , $max_length ,
array_intersect_key ( $action , array_flip ( $inherit_attrs )));
unset ( $action [ 'prefix' ]);
$action = array_diff_key ( $action , array_flip ( $inherit_attrs ));
}
// link or popup action
if ( $action [ 'url' ])
{
$action [ 'url' ] = egw :: link ( '/index.php' , str_replace ( '$action' , $id , $action [ 'url' ]));
if ( $action [ 'popup' ])
{
list ( $action [ 'data' ][ 'width' ], $action [ 'data' ][ 'height' ]) = explode ( 'x' , $action [ 'popup' ]);
unset ( $action [ 'popup' ]);
$action [ 'data' ][ 'nm_action' ] = 'popup' ;
}
else
{
$action [ 'data' ][ 'nm_action' ] = 'location' ;
}
}
if ( $action [ 'egw_open' ])
{
$action [ 'data' ][ 'nm_action' ] = 'egw_open' ;
}
// give all delete actions a delete shortcut
if ( $id === 'delete' && ! isset ( $action [ 'shortcut' ]))
{
$action [ 'shortcut' ] = egw_keymanager :: shortcut ( egw_keymanager :: DELETE );
}
static $egw_action_supported = array ( // attributes supported by egw_action
'id' , 'caption' , 'iconUrl' , 'type' , 'default' , 'onExecute' , 'group' ,
'enabled' , 'allowOnMultiple' , 'hideOnDisabled' , 'data' , 'children' ,
'hint' , 'checkbox' , 'checked' , 'radioGroup' , 'acceptedTypes' , 'dragType' ,
'shortcut'
);
// add all not egw_action supported attributes to data
$action [ 'data' ] = array_merge ( array_diff_key ( $action , array_flip ( $egw_action_supported )),( array ) $action [ 'data' ]);
if ( ! $action [ 'data' ]) unset ( $action [ 'data' ]);
// only add egw_action attributes
$egw_actions [] = array_intersect_key ( $action , array_flip ( $egw_action_supported ));
if ( ! $first_level && $n ++ == $max_length ) break ;
}
//echo "egw_actions="; _debug_array($egw_actions);
return $egw_actions ;
}
/**
* Action with submenu for categories
*
* Automatic switch to hierarchical display , if more then $max_cats_flat = 14 cats found .
*
* @ param string $app
* @ param int $group = 0 see self :: egw_actions
* @ param string $caption = 'Change category'
* @ param string $prefix = 'cat_' prefix category id to get action id
* @ param boolean $globals = true application global categories too
* @ param int $parent_id = 0 only returns cats of a certain parent
* @ param int $max_cats_flat = self :: DEFAULT_MAX_MENU_LENGTH use hierarchical display if more cats
* @ return array like self :: egw_actions
*/
public static function category_action ( $app , $group = 0 , $caption = 'Change category' ,
$prefix = 'cat_' , $globals = true , $parent_id = 0 , $max_cats_flat = self :: DEFAULT_MAX_MENU_LENGTH )
{
$cat = new categories ( null , $app );
$cats = $cat -> return_sorted_array ( $start = 0 , $limit = false , $query = '' , $sort = 'ASC' , $order = 'cat_name' , $globals , $parent_id , $unserialize_data = true );
// if more then max_length cats, switch automatically to hierarchical display
if ( count ( $cats ) > $max_cats_flat )
{
$cat_actions = self :: category_hierarchy ( $cats , $prefix , $parent_id );
}
else // flat, indented categories
{
$cat_actions = array ();
foreach (( array ) $cats as $cat )
{
$name = str_repeat ( ' ' , 2 * $cat [ 'level' ]) . stripslashes ( $cat [ 'name' ]);
if ( categories :: is_global ( $cat )) $name .= ' ♦' ;
$cat_actions [ $cat [ 'id' ]] = array (
'caption' => $name ,
'no_lang' => true ,
);
// add category icon
if ( $cat [ 'data' ][ 'icon' ] && file_exists ( EGW_SERVER_ROOT . '/phpgwapi/images/' . basename ( $cat [ 'data' ][ 'icon' ])))
{
$cat_actions [ $cat [ 'id' ]][ 'iconUrl' ] = $GLOBALS [ 'egw_info' ][ 'server' ][ 'webserver_url' ] . '/phpgwapi/images/' . $cat [ 'data' ][ 'icon' ];
}
}
}
return array (
'caption' => $caption ,
'children' => $cat_actions ,
'enabled' => ( boolean ) $cat_actions ,
'group' => $group ,
'prefix' => $prefix ,
);
}
/**
* Return one level of the category hierarchy
*
* @ param array $cats = null all cats if already read
* @ param string $prefix = 'cat_' prefix category id to get action id
* @ param int $parent_id = 0 only returns cats of a certain parent
* @ return array
*/
private static function category_hierarchy ( array $cats , $prefix , $parent_id = 0 )
{
$cat_actions = array ();
foreach ( $cats as $key => $cat )
{
// current hierarchy level
if ( $cat [ 'parent' ] == $parent_id )
{
$name = stripslashes ( $cat [ 'name' ]);
if ( categories :: is_global ( $cat )) $name .= ' ♦' ;
$cat_actions [ $cat [ 'id' ]] = array (
'caption' => $name ,
'no_lang' => true ,
'prefix' => $prefix ,
);
// add category icon
if ( $cat [ 'data' ][ 'icon' ] && file_exists ( EGW_SERVER_ROOT . '/phpgwapi/images/' . basename ( $cat [ 'data' ][ 'icon' ])))
{
$cat_actions [ $cat [ 'id' ]][ 'iconUrl' ] = $GLOBALS [ 'egw_info' ][ 'server' ][ 'webserver_url' ] . '/phpgwapi/images/' . $cat [ 'data' ][ 'icon' ];
}
unset ( $cats [ $key ]);
}
// direct children
elseif ( isset ( $cat_actions [ $cat [ 'parent' ]]))
{
$cat_actions [ 'sub_' . $cat [ 'parent' ]] = $cat_actions [ $cat [ 'parent' ]];
// have to add category itself to children, to be able to select it!
$cat_actions [ $cat [ 'parent' ]][ 'group' ] = - 1 ; // own group on top
$cat_actions [ 'sub_' . $cat [ 'parent' ]][ 'children' ] = array (
$cat [ 'parent' ] => $cat_actions [ $cat [ 'parent' ]],
) + self :: category_hierarchy ( $cats , $prefix , $cat [ 'parent' ]);
unset ( $cat_actions [ $cat [ 'parent' ]]);
}
}
return $cat_actions ;
}
2011-10-04 23:45:54 +02:00
/**
* Validate input
*
* Following attributes get checked :
* - needed : value must NOT be empty
* - min , max : int and float widget only
* - maxlength : maximum length of string ( longer strings get truncated to allowed size )
* - preg : perl regular expression incl . delimiters ( set by default for int , float and colorpicker )
* - int and float get casted to their type
*
* @ param string $cname current namespace
2012-05-03 16:17:47 +02:00
* @ param array $expand values for keys 'c' , 'row' , 'c_' , 'row_' , 'cont'
2011-10-04 23:45:54 +02:00
* @ param array $content
* @ param array & $validated = array () validated content
*/
2012-05-03 16:17:47 +02:00
public function validate ( $cname , array $expand , array $content , & $validated = array ())
2011-10-04 23:45:54 +02:00
{
2012-05-03 16:17:47 +02:00
$form_name = self :: form_name ( $cname , $this -> id , $expand );
2011-10-04 23:45:54 +02:00
$value = self :: get_array ( $content , $form_name );
2011-11-09 22:34:52 +01:00
// On client, rows does not get its own namespace, but all apps are expecting it
2012-04-10 22:27:37 +02:00
$value [ 'rows' ] = $value ;
2011-11-09 22:34:52 +01:00
2012-04-18 00:56:04 +02:00
// Legacy support - action popups were not properly namespaced
$preserve = self :: get_array ( self :: $request -> preserv , $form_name );
if ( $value [ $preserve [ 'action_var' ]] && $content [ $value [ $preserve [ 'action_var' ]] . '_popup' ])
{
$validated += $content [ $value [ $preserve [ 'action_var' ]] . '_popup' ];
}
2011-10-04 23:45:54 +02:00
// Save current column settings as default (admins only)
if ( $value [ 'as_default' ])
{
unset ( $value [ 'as_default' ]);
2011-10-05 18:12:40 +02:00
list ( $app ) = explode ( '.' , $this -> attrs [ 'template' ]);
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'admin' ] && $app )
2011-10-04 23:45:54 +02:00
{
2011-10-05 18:12:40 +02:00
$pref_name = 'nextmatch-' . ( isset ( $value [ 'columnselection_pref' ]) ? $value [ 'columnselection_pref' ] : $this -> attrs [ 'template' ] . '-details' );
// Columns already saved to user's preferences, use from there
2011-10-04 23:45:54 +02:00
$cols = $GLOBALS [ 'egw' ] -> preferences -> read ();
2011-10-05 18:12:40 +02:00
$cols = $cols [ $app ][ $pref_name ];
2011-10-04 23:45:54 +02:00
$GLOBALS [ 'egw' ] -> preferences -> add ( $app , $pref_name , is_array ( $cols ) ? implode ( ',' , $cols ) : $cols , 'default' );
2012-03-20 15:25:12 +01:00
2011-10-04 23:45:54 +02:00
$GLOBALS [ 'egw' ] -> preferences -> save_repository ( false , 'default' );
}
}
2012-04-10 22:27:37 +02:00
$validated [ $form_name ] = $value ;
2011-10-04 23:45:54 +02:00
}
2011-10-14 19:55:24 +02:00
/**
* Run a given method on all children
*
* Reimplemented to add namespace , and make sure row template gets included
*
* @ param string $method_name
2012-05-03 16:17:47 +02:00
* @ param array $params = array ( '' ) parameter ( s ) first parameter has to be cname , second $expand !
2011-10-14 19:55:24 +02:00
* @ param boolean $respect_disabled = false false ( default ) : ignore disabled , true : method is NOT run for disabled widgets AND their children
*/
public function run ( $method_name , $params = array ( '' ), $respect_disabled = false )
{
2012-03-30 18:05:29 +02:00
$old_param0 = $params [ 0 ];
2011-10-14 19:55:24 +02:00
$cname =& $params [ 0 ];
// Need this check or the headers will get involved too
2012-05-03 16:17:47 +02:00
if ( $this -> type == 'nextmatch' )
{
2011-10-14 19:55:24 +02:00
parent :: run ( $method_name , $params , $respect_disabled );
2012-05-03 20:06:27 +02:00
if ( $this -> id ) $cname = self :: form_name ( $cname , $this -> id , $params [ 1 ]);
2012-05-03 16:17:47 +02:00
if ( $this -> attrs [ 'template' ])
2011-10-14 19:55:24 +02:00
{
$row_template = etemplate_widget_template :: instance ( $this -> attrs [ 'template' ]);
$row_template -> run ( $method_name , $params , $respect_disabled );
}
}
2012-03-30 18:05:29 +02:00
$params [ 0 ] = $old_param0 ;
2012-05-15 23:43:05 +02:00
// Prevent troublesome keys from breaking the nextmatch
// TODO: Figure out where these come from
foreach ( array ( '$row' , '${row}' , '$' , '0' , '1' , '2' ) as $key )
{
if ( is_array ( self :: $request -> content [ $cname ])) unset ( self :: $request -> content [ $cname ][ $key ]);
if ( is_array ( self :: $request -> preserve [ $cname ])) unset ( self :: $request -> preserve [ $cname ][ $key ]);
}
2011-10-14 19:55:24 +02:00
}
2011-09-09 13:29:07 +02:00
}
2011-10-14 19:55:24 +02:00
// Registration needs to go here, otherwise customfields won't be loaded until some other cf shows up
etemplate_widget :: registerWidget ( 'etemplate_widget_customfields' , array ( 'nextmatch-customfields' ));
2012-06-06 18:17:44 +02:00
/**
* Extend selectbox and change type so proper users / groups get loaded , according to preferences
*/
class etemplate_widget_nextmatch_accountfilter extends etemplate_widget_menupopup
{
public function set_attrs ( $xml )
{
parent :: set_attrs ( $xml );
$this -> attrs [ 'type' ] = 'select-account' ;
}
}