* @copyright 2002-9 by RalfBecker@outdoor-training.de * @version $Id$ */ /** * eTemplate Extension: several widgets as user-interface for the link-class * * All widgets use the link-registry, to "know" which apps use popups (and what size). * Participating apps need to register a proper "search_link" hook - see eTemplate-reference (LinkWidgets) for info. * If run in a popup and the app uses no popups, a target will be set, to open a new full decorated window. * * The class contains the following widgets: * - link: Show a link to one linked entry specified by an array with keys app, id and optional title and help-message * Optionally the application can be specified as option and the value can be just the id. * - link-to: Widget to create links to an other entries of link-aware apps * If the variables $data['widget_id']['to_app'] = $app and $data['widget_id']['to_id'] = $entry_id * are set, this widget creates the links without further interaction with the calling code. * If the entry does not yet exist, the widget returns an array with the new links in the id. After the * entry was successfuly created, bolink::link($app,$new_id,$arr) has to be called to create the links! * - link-list: Widget to show the links to an entry in a table with an unlink icon for each link. Needs the same * pair of variables as link-to widget and needs to have the same id, as the data is shared with link-to. * - link-string: comma-separated list of link-titles with a link to its view method, value is like get_links() * or array with keys to_app and to_id (widget calls then get_links itself) * - link-add: Add a new entry of the select app, which is already linked to a given entry * - link-entry: Allow to select an entry of a selectable or in options specified app * - link-apps: Select an app registered in the link system, options: '' or 'add' * * * $content[$name] = array( * 'to_app' => // I string appname of the entry to link to * 'to_id' => // IO int id of the entry to link to, for new entries 0, returns the array with new links * // the following params apply only for the link-to widget! * 'no_files' => // I boolean suppress attach-files, default no * 'search_label' => // I string label to use instead of search * 'link_label' => // I string label for the link button, default 'Link' * // optional only for the link-add widget and link-entry widget * 'extra' => // I array with extra parameters, eg. array('cat_id' => 15), or string to add in onclick search for link-entry * //eg. ",values2url(this.form,'start,end,duration,participants,recur_type,whole_day')" * 'query' => // I preset for the query * 'current' => // I currently select id * // optional for link-string: * 'only_app' => // I string with appname, eg. 'projectmananager' to list only linked projects * ); * * * This widget is independent of the UI as it only uses etemplate-widgets and has therefore no render-function. */ class link_widget { /** * @var array exported methods of this class */ var $public_functions = array( 'pre_process' => True, 'post_process' => True, ); /** * @var array availible extensions and there names for the editor */ var $human_name = array( 'link' => 'Link', 'link-to' => 'LinkTo', 'link-list' => 'LinkList', 'link-string' => 'LinkString', 'link-add' => 'LinkEntry', 'link-entry' => 'Select entry', 'link-apps' => 'LinkApps', ); /** * @var boolean $debug switches debug-messages on and off */ var $debug = False; /** * Constructor of the extension * * @param string $ui '' for html */ function link_widget($ui='') { } /** * pre-processing of the 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 object &$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) { $extension_data['type'] = $type = $cell['type']; $extension_data['needed'] = $cell['needed']; $help = $cell['help'] ? ($value['help'] ? $value['help'] : $cell['help']) : lang('view this linked entry in its application'); if ((in_array($type,array('link-to','link-add','link-entry')) && !$value) && ($cell['readonly'] || $readonlys)) { //echo "

link-to is readonly, cell=".print_r($cell,true).", readonlys=".print_r($readonlys).", value='$value'

\n"; // readonly ==> omit the whole widget $value = ''; $cell = $tmpl->empty_cell(); $extension_data = null; return; } if (!is_array($value) && in_array($type,array('link-to','link-list','link-add'))) { $value = array( 'to_id' => $value, 'to_app' => $GLOBALS['egw_info']['flags']['currentapp'] ); } if ($this->debug) { echo "

link_widget::pre_process($name,$value,".print_r($cell,true).",$readonlys,,)

\n"; echo "

start: $cell[type][$name]::pre_process: value ="; _debug_array($value); echo "extension_data[$cell[type]][$name] ="; _debug_array($extension_data); } switch ($cell['type']) { case 'link': $cell['readonly'] = True; // set it readonly to NOT call our post_process function $extension_data = null; $cell['no_lang'] = 1; $link = $target = $popup = ''; if (!is_array($value) && $value && isset($GLOBALS['egw_info']['apps'][$cell['size']])) { $value = array( 'id' => $value, 'app' => $cell['size'], ); $cell['size'] = ''; } if ($value['app'] && $value['id']) { $view = egw_link::view($value['app'],$value['id']); $link = $view['menuaction']; unset($view['menuaction']); foreach($view as $var => $val) { $link .= '&'.$var.'='.$val; } if (!($popup = egw_link::is_popup($value['app'],'view')) && etemplate::$request->output_mode == 2) // we are in a popup { $target = '_blank'; } if (!$cell['help']) { $cell['help'] = $value['help']; $cell['no_lang'] = 2; } } elseif (!$value['title']) { $cell = $tmpl->empty_cell(); $cell['readonly'] = True; // set it readonly to NOT call our post_process function $extension_data = null; return; } $cell['type'] = 'label'; // size: [b[old]][i[talic]],[link],[activate_links],[label_for],[link_target],[link_popup_size],[link_title] list($cell['size']) = explode(',',$cell['size']); $cell['size'] .= ','.$link.',,,'.$target.','.$popup.','.$value['extra_title']; $value = $value['title'] ? $value['title'] : egw_link::title($value['app'],$value['id']); return true; case 'link-string': $str = ''; if ($value && !is_array($value) && $cell['size']) { $value = array('to_id' => $value); list($value['to_app'],$value['only_app']) = explode(',',$cell['size']); } if ($value['to_id'] && $value['to_app']) { $value = egw_link::get_links($value['to_app'],$value['to_id'],$only_app = $value['only_app']); if ($only_app) { foreach($value as $key => $id) { $value[$key] = array( 'id' => $id, 'app' => $only_app, ); } } } if (is_array($value)) { foreach ($value as $link) { $options .= " onMouseOver=\"self.status='".addslashes(html::htmlspecialchars($help))."'; return true;\""; $options .= " onMouseOut=\"self.status=''; return true;\""; if (($popup = egw_link::is_popup($link['app'],'view'))) { list($w,$h) = explode('x',$popup); $options = ' onclick="window.open(this,this.target,\'width='.(int)$w.',height='.(int)$h.',location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes\'); return false;"'; } elseif (etemplate::$request->output_mode == 2 || // we are in a popup $link['app'] == egw_link::VFS_APPNAME) // or it's a link to an attachment { $options = ' target="_blank"'; } $str .= ($str !== '' ? ', ' : '') . html::a_href( html::htmlspecialchars(egw_link::title($link['app'],$link['id'])), egw_link::view($link['app'],$link['id'],$link),'',$options); } } $cell['type'] = 'html'; $cell['readonly'] = True; // set it readonly to NOT call our post_process function $extension_data = null; $value = $str; return True; case 'link-add': $apps = egw_link::app_list($type == 'link-add' ? 'add_app' : 'query'); if (!$apps || !$value['to_id'] || is_array($value['to_id'])) // cant do an add without apps or already created entry { $cell = $tmpl->empty_cell(); return; } asort($apps); // sort them alphabetic $value['options-add_app'] = array(); foreach($apps as $app => $label) { $link = $GLOBALS['egw']->link('/index.php',egw_link::add($app,$value['to_app'],$value['to_id'])+ (is_array($value['extra']) ? $value['extra'] : array())); if (($popup = egw_link::is_popup($app,'add'))) { list($w,$h) = explode('x',$popup); $action = "window.open('$link','_blank','width=$w,height=$h,location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes');"; } else { $action = "location.href = '$link';"; } $value['options-add_app'][$action] = $label; // modify add_app default to the action used as value if (isset($value['add_app']) && $app == $value['add_app']) $value['add_app'] = $action; } $tpl = new etemplate('etemplate.link_widget.add'); break; case 'link-to': $GLOBALS['egw_info']['flags']['include_xajax'] = true; if ($value['search_label'] && $extension_data['search_label'] != $value['search_label']) $value['search_label'] = lang($value['search_label']); $extension_data = $value; $tpl = new etemplate('etemplate.link_widget.to'); if ($value['link_label']) $tpl->set_cell_attribute('create','label',$value['link_label']); if ($value['search_label']) $tpl->set_cell_attribute('search','label',$value['search_label']); break; case 'link-list': $app = $value['to_app']; $id = isset($extension_data['to_id']) ? $extension_data['to_id'] : $value['to_id']; if ($this->debug) { echo "

link-list-widget[$name].preprocess: value="; _debug_array($value); } if (!isset($value['title'])) { $value['title'] = egw_link::title($app,$id); } $links = egw_link::get_links($app,$id); $value['anz_links'] = count($links); $extension_data = $value; if (!count($links)) { $cell = $tmpl->empty_cell(); $value = ''; return True; } $value['link_list_format'] = $GLOBALS['egw_info']['user']['preferences']['common']['link_list_format']; $tpl = new etemplate('etemplate.link_widget.list'); for($row=$tpl->rows-1; list(,$link) = each($links); ++$row) { $value[$row] = $link; $value[$row]['title'] = egw_link::title($link['app'],$link['id'],$link); if (!is_array($link['id'])) { $value[$row]['view'] = egw_link::view($link['app'],$link['id'],$link); if (!($value[$row]['popup'] = egw_link::is_popup($link['app'],'view')) && etemplate::$request->output_mode == 2) // we are in a popup { $value[$row]['target'] = '_blank'; // we create a new window as the linked page is no popup } } if ($link['app'] == egw_link::VFS_APPNAME) { $value[$row]['target'] = '_blank'; $value[$row]['label'] = 'Delete'; $value[$row]['help'] = lang('Delete this file'); if ($value['link_list_format'] != 'text') { $value[$row]['title'] = preg_replace('/: ([^ ]+) /',': ',$value[$row]['title']); // remove mime-type, it's alread in the icon } $value[$row]['icon'] = egw_link::vfs_path($link['app2'],$link['id2'],$link['id'],true); } else { $value[$row]['icon'] = 'egw/'.$value[$row]['app']; $value[$row]['label'] = 'Unlink'; $value[$row]['help'] = lang('Remove this link (not the entry itself)'); } } break; case 'link-entry': $GLOBALS['egw_info']['flags']['include_xajax'] = true; $tpl = new etemplate('etemplate.link_widget.entry'); $options = $cell['size'] ? explode(',',$cell['size']) : array(); $app = $extension_data['app'] = $options[0]; // handle extra args for onclick like: values2url(this.form,'start,end,duration,participants,recur_type,whole_day')+'&exec[event_id]= if ( isset($value) && is_array($value) && isset($value['extra']) ) { //echo '

extra='.htmlspecialchars($value['extra'])."

\n"; //something like: values2url(this.form,'start,end,duration,participants,recur_type,whole_day')+'&exec[event_id]= $on_click_string =& $tpl->get_cell_attribute('search','onclick'); $on_click_string = str_replace(');',','.$value['extra'].');',$on_click_string); //echo htmlspecialchars($on_click_string); } if ($value) // show pre-selected entry in select-box and not the search { if (is_array($value)) { if (isset($value['current'])) { list($app,$id) = explode(':',$value['current']); } } else { // add selected-entry plus "new search" to the selectbox-options if (!isset($app) || strpos($value,':') !== false) { list($app,$id) = explode(':',$value); } else { $id = $value; } } $titles = array(); foreach(explode(',',$id) as $id) { if ($id && ($title = egw_link::title($app,$id))) { $titles[$id] = $title; } } if ($titles) { $titles[''] = lang('new search').' ...'; $selectbox =& $tpl->get_widget_by_name('id'); $selectbox['sel_options'] = $titles; // remove link_hide class from select-box-line $span =& $tpl->get_cell_attribute('select_line','span'); $span = str_replace('link_hide','',$span); // add link_hide class to search_line $span =& $tpl->get_cell_attribute('search_line','span'); $span .= ' link_hide'; unset($span); } } if ($extension_data['app'] && count($options) <= 1) // no app-selection, using app given in first option { $tpl->disable_cells('app'); $onchange =& $tpl->get_cell_attribute('search','onclick'); $onchange = str_replace("document.getElementById(form::name('app')).value",'\''.$cell['size'].'\'',$onchange); unset($onchange); } // store now our values in extension_data to preserve them upon submits (after empty title submit for example) $extension_data['default'] = $value; // adding possibility to get a default selection on app select, use for resource in calendar edit.participant $value = array( 'app' => is_array($value) && isset($value['default_sel']) ? $value['default_sel'] : $app, 'no_app_sel' => !!$extension_data['app'], 'id' => is_array($value) ? $value['current'] : $value, 'query' => is_array($value) ? $value['query'] : '', ); if ($options) // limit the app-selectbox to the given apps { $tpl->set_cell_attribute('app','type','select'); $tpl->set_cell_attribute('app','no_lang',true); $apps = egw_link::app_list('query'); asort($apps); // sort them alphabetic foreach($apps as $app => $label) { if (!in_array($app,$options)) unset($apps[$app]); } $value['options-app'] = $apps; } break; case 'link-apps': $apps = egw_link::app_list($cell['size'] ? $cell['size'] : 'query'); if (!$apps) // cant do an add without apps or already created entry { $cell = $tmpl->empty_cell(); return; } asort($apps); // sort them alphabetic $cell['sel_options'] = $apps; $cell['no_lang'] = True; // already translated $cell['type'] = 'select'; return true; } $cell['size'] = $cell['name']; $cell['type'] = 'template'; $cell['name'] = $tpl->name; $cell['obj'] =& $tpl; // keep the editor away from the generated tmpls $tpl->no_onclick = true; if ($this->debug) { echo "

end: $type"."[$name]::pre_process: value ="; _debug_array($value); } return True; // extra Label is ok } /** * postprocessing method, called after the submission of the form * * It has to copy the allowed/valid data from $value_in to $value, otherwise the widget * will return no data (if it has a preprocessing method). The framework insures that * the post-processing of all contained widget has been done before. * * Only used by select-dow so far * * @param string $name form-name of the widget * @param mixed &$value the extension returns here it's input, if there's any * @param mixed &$extension_data persistent storage between calls or pre- and post-process * @param boolean &$loop can be set to true to request a re-submision of the form/dialog * @param object &$tmpl the eTemplate the widget belongs too * @param mixed &value_in the posted values (already striped of magic-quotes) * @return boolean true if $value has valid content, on false no content will be returned! */ function post_process($name,&$value,&$extension_data,&$loop,&$tmpl,$value_in) { //echo "

link_widget::post_process('$name',value=".print_r($value,true).",ext=".print_r($extension_data,true).",$loop,,value_in=".print_r($value_in,true)."

\n"; switch($extension_data['type']) { case 'link-entry': if (!$value_in['id'] && $extension_data['needed']) { $tmpl->set_validation_error($name,lang('Field must not be empty !!!'),''); return true; } if (is_array($extension_data['default'])) { $value = $extension_data['default']; $value['current'] = $extension_data['app'] ? $value_in['id'] : $value_in['app'].':'.$value_in['id']; } else { // this was the line before the default opt, not sure it works well in all case $value = $extension_data['app'] ? $value_in['id'] : $value['app'].':'.$value_in['id']; } return true; case 'link-apps': if (!$value_in && $extension_data['needed']) { $tmpl->set_validation_error($name,lang('Field must not be empty !!!'),''); return true; } $value = $value_in; return !!$value; } $buttons = array('search','create','new','upload','attach'); while (!$button && list(,$bname) = each($buttons)) { $button = $value[$bname] ? $bname : ''; } if (is_array($value['unlink'])) { $button = 'unlink'; list($unlink) = @each($value['unlink']); } unset($value[$button]); unset($value['msg']); unset($extension_data['msg']); if (is_array($extension_data)) { $value = is_array($value) ? array_merge($extension_data,$value) : $extension_data; } if ($button && $this->debug) { echo "

start: link_widget[$name]::post_process: button='$button', unlink='$unlink', value ="; _debug_array($value); } switch ($button) { case 'create': if ($value['to_app']) // make the link { $link_id = egw_link::link($value['to_app'],$value['to_id'], $value['app'],$value['id'],$value['remark']); $value['remark'] = $value['query'] = ''; if (isset($value['primary']) && !$value['anz_links'] ) { $value['primary'] = $link_id; } } // fall-trough case 'search': case 'new': $extension_data = $value; $loop = True; break; case 'attach': if (is_array($value['file']) && $value['to_app'] && !empty($value['file']['tmp_name']) && $value['file']['tmp_name'] != 'none') { if (!$value['to_id'] || is_array($value['to_id'])) // otherwise the webserver deletes the file { if (is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir'])) { $new_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'],'egw_'); } else { $new_file = $value['file']['tmp_name'].'+'; } move_uploaded_file($value['file']['tmp_name'],$new_file); $value['file']['tmp_name'] = $new_file; } $link_id = egw_link::link($value['to_app'],$value['to_id'], egw_link::VFS_APPNAME,$value['file'],$value['remark']); $value['remark'] = ''; if (isset($value['primary']) && !$value['anz_links'] ) { $value['primary'] = $link_id; } unset($value['comment']); unset($value['file']); } else { $value['msg'] = 'You need to select a file first!'; } $extension_data = $value; $loop = True; break; case 'unlink': if ($this->debug) { //echo "

unlink(link-id=$unlink,$value[to_app],$value[to_id])

\n"; if (is_array($value['to_id'])) _debug_array($value['to_id']); } egw_link::unlink2($unlink,$value['to_app'],$value['to_id']); if (is_array($value['to_id'])) { $extension_data['to_id'] = $value['to_id']; // else changes from unlink get lost } $loop = True; break; } $value['button'] = $button; if ($this->debug) { echo "

end: link_widget[$name]::post_process: value ="; _debug_array($value); } return True; } /** * Ajax callback to search in $app for $pattern, result is displayed in $id * * Called via onClick from etemplate.link_widget.(to|entry)'s search button * * @param string $app app-name to search * @param string $pattern search-pattern * @param string $id_res id of selectbox to show the result * @param string $id_hide id(s) of the search-box/-line to hide after a successful search * @param string $id_show id(s) of the select-box/-line to show after a successful search * @param string $id_input id of the search input-field * @param string $etemplate_exec_id of the calling etemplate, to upate the allowed ids * @param string $extra optionnal extra search arguments * @return string xajax xml response */ static function ajax_search($app,$pattern,$id_res,$id_hide,$id_show,$id_input,$etemplate_exec_id,$extra=array()) { $extra_array = array(); if (!empty($extra)) { //parse $extra as a get url parse_str($extra,$extra_array) ; // securize entries as they were html encoded and so not checked on the first pass _check_script_tag($extra_array,'extra_array'); } if (empty($extra_array)) { $search = ($pattern == lang('Search'))? '' : $pattern; } else { $extra_array['search']=($pattern == lang('Search'))? '' : $pattern; $search = $extra_array; } $response = new xajaxResponse(); //$args = func_get_args(); $response->addAlert("link_widget::ajax_search('".implode("',\n'",$args)."')\n calling link->query( $app , $search )" ); //$args = func_get_args(); error_log(__METHOD__."('".implode("','",$args)."')"); if (!($found = egw_link::query($app,$search))) // ignore the blur-text { $GLOBALS['egw']->translation->add_app('etemplate'); $response->addAlert(lang('Nothing found - try again !!!')); $response->addScript("document.getElementById('$id_input').select();"); } else { $script = "var select = document.getElementById('$id_res');\nselect.options.length=0;\n"; foreach($found as $id => $option) { if (!is_array($option)) $option = array('label' => $option); $script .= "opt = select.options[select.options.length] = new Option('".addslashes($option['label'])."','".addslashes($id)."');\n"; if (count($option) > 1) { foreach($option as $name => $value) { if ($name != 'label') $script .= "opt.$name = '".addslashes($value)."';\n"; } } } $script .= "select.options[select.options.length] = new Option('".addslashes(lang('New search').' ...')."','');\n"; foreach(explode(',',$id_show) as $id) { $script .= "document.getElementById('$id').style.display='inline';\n"; } foreach(explode(',',$id_hide) as $id) { $script .= "document.getElementById('$id').style.display='none';\n"; } //$response->addAlert($script); $response->addScript($script); } // store new allowed id's in the eT request if ($etemplate_exec_id && ($request = etemplate_request::read($etemplate_exec_id))) { $data = $request->get_to_process($id_res); //error_log($id_res.'='.array2string($data)); $data['allowed'] = $found ? array_keys($found) : array(); $request->set_to_process($id_res,$data); // update id, if request changed it (happens if the request data is stored direct in the form) if ($etemplate_exec_id != ($new_id = $request->id())) { $response->addAssign('etemplate_exec_id','value',$new_id); } } return $response->getXML(); } }