- etemplate_widget_transformer abstract baseclass to define new widgets using a transformation out of existing widgets

- defines a syntax to describe how the widget is build out of existing widgets
--> reimplemented contact-widget using that aproach on serverside for old etemplate
--> sending the modifications via etemplate_widget::setElementAttribute() to the client fails, because client does not support changing the widget type
--> need to be implemented on the client
This commit is contained in:
Ralf Becker 2011-08-25 19:52:51 +00:00
parent 4114068cb3
commit 502ac42923
7 changed files with 426 additions and 40 deletions

View File

@ -1,6 +1,6 @@
<?php
/**
* eGroupWare eTemplate Extension - Contact Widget
* EGroupware eTemplate Extension - Contact Widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
@ -13,24 +13,26 @@
/**
* eTemplate Extension: Contact widget
*
* This widget can be used to fetch fields of a contact specified by contact-id
* This widget can be used to fetch fields of a contact specified by contact-id
*/
class contact_widget
class contact_widget extends etemplate_widget_transformer
{
/**
/**
* exported methods of this class
*
*
* @var array $public_functions
* @deprecated only used for old etemplate
*/
var $public_functions = array(
public $public_functions = array(
'pre_process' => 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 "<p>contact_widget::pre_process('$name','$value',".print_r($cell,true).",...)</p>\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 "<p>$name: $value</p>\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'));

View File

@ -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 "<p>set_array(\$content, '$name', ".array2string($value).")</p>\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']);

View File

@ -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(),

View File

@ -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;
}
/**

View File

@ -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'])
{

View File

@ -0,0 +1,222 @@
<?php
/**
* EGroupware - eTemplate serverside base widget, to define new widgets using a transformation out of existing widgets
*
* @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
* @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!');
}
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<!-- $Id$ -->
<overlay>
<template id="etemplate.test.contact_widget" template="" lang="" group="0" version="">
<grid>
<columns>
<column/>
</columns>
<rows>
<row>
<contact-fields id="fields"/>
</row>
<row>
<contact-account label="Benutzer" id="account:" options="n_fn"/>
</row>
<row>
<contact-account label="Telefon" id="account:" options="tel_work"/>
</row>
</rows>
</grid>
</template>
</overlay>