diff --git a/etemplate/inc/class.contact_widget.inc.php b/etemplate/inc/class.contact_widget.inc.php index 6281aeffa5..91cdfd5baa 100644 --- a/etemplate/inc/class.contact_widget.inc.php +++ b/etemplate/inc/class.contact_widget.inc.php @@ -1,6 +1,6 @@ True, ); /** * availible extensions and there names for the editor * - * @var string/array $human_name + * @var string|array $human_name + * @deprecated only used for old etemplate */ - var $human_name = array( + public $human_name = array( 'contact-value' => 'Contact', 'contact-account' => 'Account contactdata', 'contact-template' => 'Account template', @@ -38,27 +40,147 @@ class contact_widget ); /** * Instance of the contacts class - * + * * @var contacts */ - var $contacts; + private $contacts; + /** - * Cached contact + * Array with a transformation description, based on attributes to modify. + * + * Exampels: + * + * * 'type' => array('some' => 'other') + * if 'type' attribute equals 'some' replace it with 'other' + * + * * 'type' => array('some' => array('type' => 'other', 'options' => 'otheroption') + * same as above, but additonally set 'options' attr to 'otheroption' + * + * --> leaf element is the action, if previous filters are matched: + * - if leaf is scalar, it just replaces the previous filter value + * - if leaf is an array, it contains assignments for (multiple) attributes: attr => value pairs + * + * * 'type' => array( + * 'some' => array(...), + * 'other' => array(...), + * '__default__' => array(...), + * ) + * it's possible to have a list of filters with actions to run, plus a '__default__' which matches all not explicitly named values + * + * * 'value' => array('__callback__' => 'app.class.method' || 'class::method' || 'method') + * run value through a *serverside* callback, eg. reading an entry based on it's given id + * + * * 'value' => array('__js__' => 'function(value) { return value+5; }') + * run value through a *clientside* callback running in the context of the widget + * + * * 'name' => '@name[@options]' + * replace value of 'name' attribute with itself (@name) plus value of options in square brackets + * + * --> attribute name prefixed with @ sign means value of given attribute * * @var array */ - var $contact; - + protected static $transformation = array( + 'type' => array( + 'contact-fields' => array( // contact-fields widget + 'sel_options' => array('__callback__' => 'get_contact_fields'), + 'type' => 'select', + 'no_lang' => true, + 'options' => 'None', + ), + 'contact-template' => array( + 'value' => array('__callback__' => 'get_contact'), + 'type' => 'template', + 'options' => '', + 'label' => array( + '' => array('id' => '@value[@id]'), + '__default__' => array('id' => '@label@value[@id]'), // non-empty label prefixes value + ), + ), + '__default__' => array( + 'value' => array('__callback__' => 'get_contact'), + 'id' => '@id[@options]', + 'options' => array( + '' => array('id' => '@value[@id]'), + 'bday' => array('type' => 'date', 'options' => 'Y-m-d'), + 'owner' => array('type' => 'select-account', 'options' => ''), + 'modifier' => array('type' => 'select-account', 'options' => ''), + 'creator' => array('type' => 'select-account', 'options' => ''), + 'modifed' => array('type' => 'date-time', 'options' => ''), + 'created' => array('type' => 'date-time', 'options' => ''), + 'cat_id' => array('type' => 'select-cat', 'options' => ''), + '__default__' => array('type' => 'label', 'options' => ''), + ), + 'readonly' => true, + 'no_lang' => 1, + ), + ), + ); + /** * Constructor of the extension * * @param string $ui '' for html */ - function contact_widget($ui) + function __construct($xml) { - $this->ui = $ui; + if (is_a($xml, 'XMLReader') || $xml != '') + { + parent::__construct($xml); + } + $this->contacts = $GLOBALS['egw']->contacts; + } - $this->contacts =& $GLOBALS['egw']->contacts; + /** + * Get all contact-fields + * + * @return array + */ + public function get_contact_fields() + { + translation::add_app('addressbook'); + $this->contacts->__construct(); + $options = $this->contacts->contact_fields; + foreach($this->contacts->customfields as $name => $data) + { + $options['#'.$name] = $data['label']; + } + return $options; + } + + /** + * Get contact data, if $value not already contains them + * + * @param int|string|array $value + * @param array $attrs + * @return array + */ + public function get_contact($value, array $attrs) + { + if (is_array($value)) return $value; + + switch($attrs['type']) + { + case 'contact-account': + case 'contact-template': + if (substr($value,0,8) != 'account:') + { + $value = 'account:'.($attrs['name'] != 'account:' ? $value : $GLOBALS['egw_info']['user']['account_id']); + } + // fall-throught + case 'contact-value': + default: + if (substr($value,0,12) == 'addressbook:') $value = substr($value,12); // link-entry syntax + if (!($contact = $this->contacts->read($value))) + { + $contact = array(); + } + break; + } + unset($contact['jpegphoto']); // makes no sense to return binary image + + //error_log(__METHOD__."('$value') returning ".array2string($contact)); + return $contact; } /** @@ -68,25 +190,21 @@ class contact_widget * * @param string $name form-name of the control * @param mixed &$value value / existing content, can be modified - * @param array &$cell array with the widget, can be modified for ui-independent widgets + * @param array &$cell array with the widget, can be modified for ui-independent widgets * @param array &$readonlys names of widgets as key, to be made readonly * @param mixed &$extension_data data the extension can store persisten between pre- and post-process * @param etemplate &$tmpl reference to the template we belong too * @return boolean true if extra label is allowed, false otherwise */ - function pre_process($name,&$value,&$cell,&$readonlys,&$extension_data,&$tmpl) +/* old code now replaced with etemplate_widget_transformer::pre_process() ... + + function pre_process($name,&$value,&$cell,&$readonlys,&$extension_data,&$tmpl) { //echo "

contact_widget::pre_process('$name','$value',".print_r($cell,true).",...)

\n"; switch($type = $cell['type']) { case 'contact-fields': - $GLOBALS['egw']->translation->add_app('addressbook'); - $this->contacts->__construct(); - $cell['sel_options'] = $this->contacts->contact_fields; - foreach($this->contacts->customfields as $name => $data) - { - $cell['sel_options']['#'.$name] = $data['label']; - } + $cell['sel_options'] = $this->get_contact_fields(); $cell['type'] = 'select'; $cell['no_lang'] = 1; $cell['size'] = 'None'; @@ -98,11 +216,12 @@ class contact_widget { $value = 'account:'.($cell['name'] != 'account:' ? $value : $GLOBALS['egw_info']['user']['account_id']); } + echo "

$name: $value

\n"; // fall-throught case 'contact-value': default: if (substr($value,0,12) == 'addressbook:') $value = substr($value,12); // link-entry syntax - if (!$value || !$cell['size'] || (!is_array($this->contact) || + if (!$value || !$cell['size'] || (!is_array($this->contact) || !($this->contact['id'] == $value || 'account:'.$this->contact['account_id'] == $value)) && !($this->contact = $this->contacts->read($value))) { @@ -112,7 +231,7 @@ class contact_widget } $type = $cell['size']; $cell['size'] = ''; - + if ($cell['type'] == 'contact-template') { $name = $this->contact[$type]; @@ -124,27 +243,27 @@ class contact_widget $value = $this->contact[$type]; $cell['no_lang'] = 1; $cell['readonly'] = true; - + switch($type) { // ToDo: pseudo types like address-label - + case 'bday': $cell['type'] = 'date'; $cell['size'] = 'Y-m-d'; break; - + case 'owner': case 'modifier': case 'creator': $cell['type'] = 'select-account'; break; - + case 'modified': case 'created': $cell['type'] = 'date-time'; break; - + case 'cat_id': $cell['type'] = 'select-cat'; break; @@ -158,5 +277,7 @@ class contact_widget $cell['id'] = ($cell['id'] ? $cell['id'] : $cell['name'])."[$type]"; return True; // extra label ok - } + }*/ } +// register widgets for etemplate2 +etemplate_widget::registerWidget('contact_widget',array('contact-value', 'contact-account', 'contact-template', 'contact-fields')); diff --git a/etemplate/inc/class.etemplate.inc.php b/etemplate/inc/class.etemplate.inc.php index 9d96c541c6..6e7ce1fee7 100644 --- a/etemplate/inc/class.etemplate.inc.php +++ b/etemplate/inc/class.etemplate.inc.php @@ -1122,12 +1122,28 @@ class etemplate extends boetemplate { $readonlys[$name] = true; } + $cell_name = $cell['name']; $extra_label = $this->extensionPreProcess($type,$form_name,$value,$cell,$readonlys[$name]); $readonly = $cell['readonly'] !== false && ($readonly || $cell['readonly']); // might be set or unset (===false) by extension - +//echo "

set_array(\$content, '$name', ".array2string($value).")

\n"; self::set_array($content,$name,$value); + // if widget changes the name (eg. by new widget-transformer), reevaluate name, form_name and value + if ($cell['name'] && $cell['name'] != $cell_name) + { + $name = $this->expand_name($cell['name'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + // allow names like "tabs=one|two|three", which will be equal to just "tabs" + // eg. for tabs to use a name independent of the tabs contained + if (is_string($name) && strpos($name,'=') !== false) + { + list($name) = explode('=',$name); + } + $form_name = self::form_name($cname,$name); + + $value = $this->get_array($content,$name); + } + if ($cell['type'] == $type.'-'.$sub_type) break; // stop if no further type-change list($type,$sub_type) = explode('-',$cell['type']); diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 2b85ea83f7..5eff0ac22b 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -129,7 +129,7 @@ class etemplate_new extends etemplate_widget_template // instanciate template to fill self::$request->sel_options for select-* widgets // not sure if we want to handle it this way, thought otherwise we will have a few ajax request for each dialog fetching predefined selectboxes $template = etemplate_widget_template::instance($this->name, $this->template_set, $this->version, $this->laod_via); - $template->run('fillTypeOptions'); + $template->run('beforeSendToClient'); $data = array( 'etemplate_exec_id' => self::$request->id(), diff --git a/etemplate/inc/class.etemplate_widget.inc.php b/etemplate/inc/class.etemplate_widget.inc.php index a8958ae817..0bf258abfd 100644 --- a/etemplate/inc/class.etemplate_widget.inc.php +++ b/etemplate/inc/class.etemplate_widget.inc.php @@ -15,6 +15,7 @@ require_once EGW_INCLUDE_ROOT.'/etemplate/inc/class.etemplate_widget_textbox.inc.php'; require_once EGW_INCLUDE_ROOT.'/etemplate/inc/class.etemplate_widget_grid.inc.php'; require_once EGW_INCLUDE_ROOT.'/etemplate/inc/class.etemplate_widget_checkbox.inc.php'; +require_once EGW_INCLUDE_ROOT.'/etemplate/inc/class.contact_widget.inc.php'; /** * eTemplate widget baseclass @@ -234,6 +235,10 @@ class etemplate_widget */ public static function registerWidget($class, $widgets) { + if (!is_subclass_of($class, __CLASS__)) + { + throw new egw_exception_wrong_parameter(__METHOD__."('$class', ".array2string($widgets).") $class is no subclass or ".__CLASS__.'!'); + } foreach((array)$widgets as $widget) { self::$widget_registry[$widget] = $class; @@ -676,11 +681,11 @@ class etemplate_widget */ public function &setElementAttribute($name,$attr,$val) { - $attr =& self::$request->modifications[$name][$attr]; - if (!is_null($val)) $attr = $val; + $ref =& self::$request->modifications[$name][$attr]; + if (!is_null($val)) $ref = $val; error_log(__METHOD__."('$name', '$attr', ".array2string($val).')'); - return $attr; + return $ref; } /** diff --git a/etemplate/inc/class.etemplate_widget_menupopup.inc.php b/etemplate/inc/class.etemplate_widget_menupopup.inc.php index bed2474fed..c424036dcc 100644 --- a/etemplate/inc/class.etemplate_widget_menupopup.inc.php +++ b/etemplate/inc/class.etemplate_widget_menupopup.inc.php @@ -78,7 +78,7 @@ class etemplate_widget_menupopup extends etemplate_widget $value = $value_in = self::get_array($content, $form_name); $allowed = $this->attrs['multiple'] ? array() : array('' => $this->attrs['options']); - /* if fillTypeOptions is used, we dont need to call it again here + /* if beforeSendToClient is used, we dont need to call it again here if ($this->attrs['type']) { $allowed += self::typeOptions($form_name, $this->attrs['type'], $this->attrs['no_lang']); @@ -114,7 +114,7 @@ class etemplate_widget_menupopup extends etemplate_widget * * @param string $cname */ - public function fillTypeOptions($cname) + public function beforeSendToClient($cname) { if ($this->attrs['type']) { diff --git a/etemplate/inc/class.etemplate_widget_transformer.inc.php b/etemplate/inc/class.etemplate_widget_transformer.inc.php new file mode 100644 index 0000000000..31bdfa7710 --- /dev/null +++ b/etemplate/inc/class.etemplate_widget_transformer.inc.php @@ -0,0 +1,222 @@ + + * @copyright 2002-11 by RalfBecker@outdoor-training.de + * @version $Id$ + */ + +/** + * eTemplate serverside base widget, to define new widgets using a transformation out of existing widgets + */ +abstract class etemplate_widget_transformer extends etemplate_widget +{ + /** + * Array with a transformation description, based on attributes to modify. + * + * Exampels: + * + * * 'type' => array('some' => 'other') + * if 'type' attribute equals 'some' replace it with 'other' + * + * * 'type' => array('some' => array('type' => 'other', 'options' => 'otheroption') + * same as above, but additonally set 'options' attr to 'otheroption' + * + * --> leaf element is the action, if previous filters are matched: + * - if leaf is scalar, it just replaces the previous filter value + * - if leaf is an array, it contains assignments for (multiple) attributes: attr => value pairs + * + * * 'type' => array( + * 'some' => array(...), + * 'other' => array(...), + * '__default__' => array(...), + * ) + * it's possible to have a list of filters with actions to run, plus a '__default__' which matches all not explicitly named values + * + * * 'value' => array('__callback__' => 'app.class.method' || 'class::method' || 'method') + * run value through a *serverside* callback, eg. reading an entry based on it's given id + * callback signature: mixed function(mixed $attr[, array $attrs]) + * + * * 'value' => array('__js__' => 'function(value) { return value+5; }') + * run value through a *clientside* callback running in the context of the widget + * + * * 'name' => '@name[@options]' + * replace value of 'name' attribute with itself (@name) plus value of options in square brackets + * * 'value' => '@value[@options]' + * replace value array with value for key taken from value of options attribute + * + * --> attribute name prefixed with @ sign means value of given attribute + * + * @var array + */ + protected static $transformation = array(); + + /** + * Switching debug messages to error_log on/off + * + * @var boolean + */ + const DEBUG = true; + + /** + * Rendering transformer widget serverside as an old etemplate extension + * + * This function is called before the extension gets rendered + * + * @param string $name form-name of the control + * @param mixed &$value value / existing content, can be modified + * @param array &$cell array with the widget, can be modified for ui-independent widgets + * @param array &$readonlys names of widgets as key, to be made readonly + * @param mixed &$extension_data data the extension can store persisten between pre- and post-process + * @param etemplate &$tmpl reference to the template we belong too + * @return boolean true if extra label is allowed, false otherwise + */ + function pre_process($name,&$value,&$cell,&$readonlys,&$extension_data,&$tmpl) + { + $cell['value'] =& $value; + $cell['options'] =& $cell['size']; // old engine uses 'size' instead of 'options' for legacy options + $cell['id'] =& $cell['name']; // dto for 'name' instead of 'id' + + // run the transformation + foreach(static::$transformation as $filter => $data) + { + $this->action($filter, $data, $cell); + } + return true; + } + + /** + * Fill type options in self::$request->sel_options to be used on the client + * + * @param string $cname + */ + public function beforeSendToClient($cname) + { + $attrs = $this->attrs; + $form_name = self::form_name($cname, $this->id); + if (empty($this->id)) + { + error_log(__METHOD__."() $this has no id!"); + return; + } + $attrs['value'] = $value =& self::get_array(self::$request->content, $form_name); + $attrs['type'] = $this->type; + $attrs['id'] = $this->id; + + $unmodified = $attrs; + + // run the transformation + foreach(static::$transformation as $filter => $data) + { + $this->action($filter, $data, $attrs); + } + + //echo $this; _debug_array($unmodified); _debug_array($attrs); _debug_array(array_diff_assoc($attrs, $unmodified)); + // compute the difference and send it to the client as modifications + foreach(array_diff_assoc($attrs, $unmodified) as $attr => $val) + { + switch($attr) + { + case 'value': + if ($val != $value) + { + $value = $val; // $value is reference to self::$request->content + } + break; + case 'sel_options': + self::$request->sel_options[$form_name] = $val; + break; + case 'type': // not an attribute in etempalte2 + default: + self::setElementAttribute($form_name, $attr, $val); + break; + } + } + } + + /** + * Recursively run given action(s) on an attribute value + * + * @param string $attr attribute concerned + * @param int|string|array $action action to run + * @param array &$attrs attributes + * @throws egw_exception_wrong_parameter if $action is of wrong type + */ + function action($attr, $action, array &$attrs) + { + if (self::DEBUG) error_log(__METHOD__."('$attr', ".array2string($action).')'); + // action is an assignment + if (is_scalar($action) || is_null($action)) + { + // check if assignment contains placeholders --> replace them + if (strpos($action, '@') !== false) + { + $replace = array(); + foreach($attrs as $a => $v) + { + if (is_scalar($v) || is_null($v)) $replace['@'.$a] = $v; + } + $action = strtr($action, $replace); + // now replace with non-scalar value, eg. if values is an array: "@value", "@value[key] or "@value[@key]" + if (($a = strstr($action, '@'))) + { + $action = self::get_array($attrs, substr($a,1)); + } + } + $attrs[$attr] = $action; + if (self::DEBUG) error_log(__METHOD__."('$attr', ".array2string($action).") attrs['$attr'] = ".array2string($action).', attrs='.array2string($attrs)); + } + // action is a serverside callback + elseif(is_array($action) && isset($action['__callback__'])) + { + if (!is_string(($callback = $action['__callback__']))) + { + throw new egw_exception_wrong_parameter(__METHOD__."('$attr', ".array2string($action).', '.array2string($attrs).') wrong datatype for callback!'); + } + if (method_exists($this, $callback)) + { + $attrs[$attr] = $this->$callback($attrs[$attr], $attrs); + } + elseif(count(explode('.', $callback)) == 3) + { + $attrs[$attr] = ExecMethod($callback, $attrs[$attr], $attrs); + } + elseif (is_callable($callback, false)) + { + $attrs[$attr] = call_user_func($callback, $attrs[$attr], $attrs); + } + else + { + throw new egw_exception_wrong_parameter(__METHOD__."('$attr', ".array2string($action).', '.array2string($attrs).') wrong datatype for callback!'); + } + } + // action is a clientside callback + elseif(is_array($action) && isset($action['__js__'])) + { + // nothing to do here + } + // action is a switch --> check cases + elseif(is_array($action)) + { + // case matches --> run all actions + if (isset($action[$attrs[$attr]]) || !isset($action[$attrs[$attr]]) && isset($action['__default__'])) + { + $actions = isset($action[$attrs[$attr]]) ? $action[$attrs[$attr]] : $action['__default__']; + if (self::DEBUG) error_log(__METHOD__."(attr='$attr', action=".array2string($action).") attrs['$attr']=='{$attrs[$attr]}' --> running actions"); + foreach($actions as $attr => $action) + { + $this->action($attr, $action, $attrs); + } + } + } + else + { + throw new egw_exception_wrong_parameter(__METHOD__."(attr='$attr', action=".array2string($action).', attrs='.array2string($attrs).') wrong datatype for action!'); + } + } +} diff --git a/etemplate/templates/default/test.contact_widget.xet b/etemplate/templates/default/test.contact_widget.xet new file mode 100644 index 0000000000..f7a30b432a --- /dev/null +++ b/etemplate/templates/default/test.contact_widget.xet @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file