diff --git a/etemplate/inc/class.etemplate_widget_menupopup.inc.php b/etemplate/inc/class.etemplate_widget_menupopup.inc.php index f49d56d2de..868706cab5 100644 --- a/etemplate/inc/class.etemplate_widget_menupopup.inc.php +++ b/etemplate/inc/class.etemplate_widget_menupopup.inc.php @@ -23,6 +23,27 @@ class etemplate_widget_menupopup extends etemplate_widget */ const SEARCH_ROW_LIMIT = PHP_INT_MAX; // Automatic disabled, only explicit + /** + * These types are either set or cached on the client side, so we don't send + * their options unless asked via AJAX + */ + public static $cached_types = array( + 'select-account', + 'select-app', + 'select-bool', + 'select-country', + 'select-dow', + 'select-number', + 'select-priority', + 'select-percent', + 'select-year', + 'select-month', + 'select-day', + 'select-hour', + 'select-lang', + 'select-timezone' + ); + /** * @var array */ @@ -42,6 +63,19 @@ class etemplate_widget_menupopup extends etemplate_widget 12 => 'December' ); + /** + * Constructor + * + * @param string|XMLReader $xml string with xml or XMLReader positioned on the element to construct + * @throws egw_exception_wrong_parameter + */ + public function __construct($xml = '') + { + if($xml) { + parent::__construct($xml); + } + } + /** * Parse and set extra attributes from xml in template object * @@ -92,6 +126,11 @@ class etemplate_widget_menupopup extends etemplate_widget $value = $value_in = self::get_array($content, $form_name); $allowed = self::selOptions($form_name, true); // true = return array of option-values + $type_options = self::typeOptions($this, + // typeOptions thinks # of rows is the first thing in options + ($this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options'])); + $allowed = array_merge($allowed,array_keys($type_options)); + if (!$this->attrs['multiple'] || !($this->attrs['options'] > 1)) $allowed[] = ''; foreach((array) $value as $val) @@ -232,30 +271,32 @@ class etemplate_widget_menupopup extends etemplate_widget unset(self::$request->content[$this->id]); $this->attrs['readonly'] = true; } - - // adding type specific options here, while keep further options set by app code - // we need to make sure to run only once for auto-repeated rows, because - // array_merge used to keep options from app would otherwise add - // type-specific ones multiple time (and of cause better performance) - $no_lang = null; - static $form_names_done = array(); - if (!isset($form_names_done[$form_name]) && - ($type_options = self::typeOptions($this, - // typeOptions thinks # of rows is the first thing in options - ($this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options']), - $no_lang, $this->attrs['readonly'], self::get_array(self::$request->content, $form_name), $form_name))) + if(!in_array($this->attrs['type'], self::$cached_types)) { - self::fix_encoded_options($type_options); - - self::$request->sel_options[$form_name] = array_merge(self::$request->sel_options[$form_name], $type_options); - - // if no_lang was modified, forward modification to the client - if ($no_lang != $this->attr['no_lang']) + // adding type specific options here, while keep further options set by app code + // we need to make sure to run only once for auto-repeated rows, because + // array_merge used to keep options from app would otherwise add + // type-specific ones multiple time (and of cause better performance) + $no_lang = null; + static $form_names_done = array(); + if (!isset($form_names_done[$form_name]) && + ($type_options = self::typeOptions($this, + // typeOptions thinks # of rows is the first thing in options + ($this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options']), + $no_lang, $this->attrs['readonly'], self::get_array(self::$request->content, $form_name), $form_name))) { - self::setElementAttribute($form_name, 'no_lang', $no_lang); + self::fix_encoded_options($type_options); + + self::$request->sel_options[$form_name] = array_merge(self::$request->sel_options[$form_name], $type_options); + + // if no_lang was modified, forward modification to the client + if ($no_lang != $this->attr['no_lang']) + { + self::setElementAttribute($form_name, 'no_lang', $no_lang); + } } + $form_names_done[$form_name] = true; } - $form_names_done[$form_name] = true; } // Make sure  s, etc. are properly encoded when sent, and not double-encoded @@ -431,15 +472,16 @@ class etemplate_widget_menupopup extends etemplate_widget { $widget = $widget_type; $widget_type = $widget->attrs['type'] ? $widget->attrs['type'] : $widget->type; - // Legacy / static support - // Have to do this explicitly, since legacy options is not defined on class level - $legacy_options = explode(',',$_legacy_options); - foreach($legacy_options as &$field) - { - $field = self::expand_name($field, 0, 0,'','',self::$cont); - } - list($rows,$type,$type2,$type3,$type4,$type5) = $legacy_options; } + // Legacy / static support + // Have to do this explicitly, since legacy options is not defined on class level + $legacy_options = explode(',',$_legacy_options); + foreach($legacy_options as &$field) + { + $field = self::expand_name($field, 0, 0,'','',self::$cont); + } + + list($rows,$type,$type2,$type3,$type4,$type5) = $legacy_options; $no_lang = false; $options = array(); switch ($widget_type) @@ -466,14 +508,6 @@ class etemplate_widget_menupopup extends etemplate_widget $options = array(0 => 'no',1 => 'yes'); break; - case 'select-access': - $options = array( - 'private' => 'Private', - 'public' => 'Global public', - 'group' => 'Group public' - ); - break; - case 'select-country': // #Row|Extralabel,1=use country name, 0=use 2 letter-code,custom country field name if($type == 0 && $type2) { @@ -790,6 +824,22 @@ class etemplate_widget_menupopup extends etemplate_widget } return $info; } + + /** + * Some select options are fairly static, but can only be generated on the server + * so we generate them here, then cache them client-side + * + * @param string $type + * @param Array|String $attributes + * + */ + public static function ajax_get_options($type, $attributes) + { + $options = self::typeOptions($type, $attributes); + self::fix_encoded_options($options,true); + $response = egw_json_response::get(); + $response->data($options); + } } etemplate_widget::registerWidget('etemplate_widget_menupopup', array('selectbox','listbox','select','menupopup')); diff --git a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php index 5dde80c4bd..9729eb53d3 100644 --- a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php +++ b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php @@ -1206,6 +1206,10 @@ class etemplate_widget_nextmatch_customfilter extends etemplate_widget_transform list($type) = explode('-',$this->attrs['type']); if($type == 'select') { + if(in_array($this->attrs['type'], etemplate_widget_menupopup::$cached_types)) + { + $widget_type = $this->attrs['type']; + } $this->attrs['type'] = 'nextmatch-filterheader'; } self::$transformation['type'] = $this->attrs['type']; @@ -1215,6 +1219,10 @@ class etemplate_widget_nextmatch_customfilter extends etemplate_widget_transform $this->setElementAttribute($form_name, 'options', trim($this->attrs['widget_options']) != '' ? $this->attrs['widget_options'] : ''); $this->setElementAttribute($form_name, 'type', $this->attrs['type']); + if($widget_type) + { + $this->setElementAttribute($form_name, 'widget_type', $widget_type); + } parent::beforeSendToClient($cname, $expand); } } diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index 3968513c94..66b3d2db8e 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -3226,12 +3226,14 @@ var et2_nextmatch_customfilter = et2_nextmatch_filterheader.extend( "widget_type": { "name": "Actual type", "type": "string", - "description": "The actual type of widget you should use" + "description": "The actual type of widget you should use", + "no_lang": 1 }, "widget_options": { "name": "Actual options", "type": "any", "description": "The options for the actual widget", + "no_lang": 1, "default": {} } }, @@ -3247,7 +3249,6 @@ var et2_nextmatch_customfilter = et2_nextmatch_filterheader.extend( * @memberOf et2_nextmatch_customfilter */ init: function(_parent, _attrs) { - this._super.apply(this, arguments); switch(_attrs.widget_type) { @@ -3255,11 +3256,26 @@ var et2_nextmatch_customfilter = et2_nextmatch_filterheader.extend( _attrs.type = 'nextmatch-entryheader'; break; default: - _attrs.type = _attrs.widget_type; + if(_attrs.widget_type.indexOf('select') === 0) + { + _attrs.type = 'nextmatch-filterheader'; + } + else + { + _attrs.type = _attrs.widget_type; + } } - // Avoid warning about non-existant attribute - delete(_attrs.widget_type); + jQuery.extend(_attrs.widget_options,{id: this.id}); + + _attrs.id = ''; + this._super.apply(this, arguments); this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this._parent); + var select_options = []; + var correct_type = _attrs.type; + this.real_node._type = _attrs.widget_type; + et2_selectbox.find_select_options(this.real_node, select_options, _attrs); + this.real_node._type = correct_type; + this.real_node.set_select_options(select_options); }, // Just pass the real DOM node through, in case anybody asks diff --git a/etemplate/js/et2_widget_selectbox.js b/etemplate/js/et2_widget_selectbox.js index 2c17c14a64..5fe57e0614 100644 --- a/etemplate/js/et2_widget_selectbox.js +++ b/etemplate/js/et2_widget_selectbox.js @@ -170,7 +170,7 @@ var et2_selectbox = et2_inputWidget.extend( return; } - var sel_options = et2_selectbox.find_select_options(this, _attrs['select_options']); + var sel_options = et2_selectbox.find_select_options(this, _attrs['select_options'], _attrs); if(!jQuery.isEmptyObject(sel_options)) { _attrs['select_options'] = sel_options; @@ -748,7 +748,7 @@ var et2_selectbox = et2_inputWidget.extend( } }); et2_register_widget(et2_selectbox, ["menupopup", "listbox", "select", "select-cat", - "select-percent", 'select-priority', 'select-access', + "select-percent", 'select-priority', 'select-country', 'select-state', 'select-year', 'select-month', 'select-day', 'select-dow', 'select-hour', 'select-number', 'select-app', 'select-lang', 'select-bool', 'select-timezone' ]); @@ -756,20 +756,35 @@ et2_register_widget(et2_selectbox, ["menupopup", "listbox", "select", "select-ca // Static class stuff jQuery.extend(et2_selectbox, { + type_cache: {}, + /** * Find the select options for a widget, out of the many places they could be. * @param {et2_widget} widget to check for. Should be some sort of select widget. * @param {object} attr_options Select options in attributes array + * @param {object} attrs Widget attributes * @return {object} Select options, or empty object */ - find_select_options: function(widget, attr_options) + find_select_options: function(widget, attr_options, attrs) { var name_parts = widget.id.replace(/[/g,'[').replace(/]|]/g,'').split('['); var content_options = {}; - // Try to find the options inside the "sel-options" array - if(widget.getArrayMgr("sel_options")) + // First check type, there may be static options. There's some special handling + // for filterheaders, which have the wrong type. + var type = widget.instanceOf(et2_nextmatch_filterheader) ? attrs.widget_type || '' : widget._type; + var type_function = type.replace('select-','').replace('_ro','')+'_options'; + if(typeof this[type_function] == 'function') + { + var old_type = widget._type; + widget._type = type; + content_options = this[type_function].call(this, widget, attrs); + widget._type = old_type; + } + + // Try to find the options inside the "sel-options" + if(jQuery.isEmptyObject(content_options) && widget.getArrayMgr("sel_options")) { // Try first according to ID content_options = widget.getArrayMgr("sel_options").getEntry(widget.id); @@ -860,6 +875,199 @@ jQuery.extend(et2_selectbox, content_options = {}; } return content_options; + }, + + /** + * Some static options, no need to transfer them over and over. + * We still need the same thing on the server side to validate, so they + * have to match. See etemplate_widget_menupopup::typeOptions() + * The type specific legacy options wind up in attrs.other. + */ + priority_options: function(widget) { + return [ + {value: 1, label: 'low'}, + {value: 2, label: 'normal'}, + {value: 3, label: 'high'} + ]; + }, + bool_options: function(widget) { + return [ + {value: 0, label: 'no'}, + {value: 1, label: 'yes'} + ]; + }, + month_options: function(widget) { + return [ + {value: 1, label:'January'}, + {value: 2, label:'February'}, + {value: 3, label:'March'}, + {value: 4, label:'April'}, + {value: 5, label:'May'}, + {value: 6, label:'June'}, + {value: 7, label:'July'}, + {value: 8, label:'August'}, + {value: 9, label:'September'}, + {value: 10, label:'October'}, + {value: 11, label:'November'}, + {value: 12, label:'December'} + ]; + }, + number_options: function(widget, attrs) { + if(typeof attrs.other != 'object') + { + attrs.other = []; + } + var options = []; + var min = typeof(attrs.other[0]) == 'undefined' ? 1 : parseInt(attrs.other[0]); + var max = typeof(attrs.other[1]) == 'undefined' ? 10: parseInt(attrs.other[1]); + var interval = typeof(attrs.other[2]) == 'undefined' ? 1: parseInt(attrs.other[2]); + var format = '%d'; + + // leading zero specified in interval + if (attrs.other[2] && attrs.other[2][0] == '0') + { + format = '%0'+(''+interval).length+'d'; + } + // Suffix + if(attrs.other[3]) + { + format += widget.egw().lang(attrs.other[3]); + } + + // Avoid infinite loop if interval is the wrong direction + if ((min <= max) != (interval > 0)) + { + interval = -interval; + } + + for (var i=0, n=min; n <= max && i <= 100; n += interval,++i) + { + options.push({value: n, label: sprintf(format,n)}); + } + return options; + }, + percent_options: function(widget, attrs) + { + if(typeof attrs.other != 'object') + { + attrs.other = []; + } + attrs.other[0] = 0; + attrs.other[1] = 100; + attrs.other[2] = typeof(attrs.other[2]) == 'undefined' ? 10 : parseInt(attrs.other[2]); + attrs.other[3] = '%%'; + return this.number_options(widget,attrs); + }, + year_options: function(widget, attrs) + { + if(typeof attrs.other != 'object') + { + attrs.other = []; + } + var t = new Date(); + attrs.other[0] = t.getFullYear() - (typeof(attrs.other[0]) == 'undefined' ? 3 : parseInt(attrs.other[0])); + attrs.other[1] = t.getFullYear() + (typeof(attrs.other[1]) == 'undefined' ? 2 : parseInt(attrs.other[1])); + attrs.other[2] = typeof(attrs.other[2]) == 'undefined' ? 1 : parseInt(attrs.other[2]); + return this.number_options(widget,attrs); + }, + day_options: function(widget, attrs) + { + attrs.other = [1,31,1]; + return this.number_options(widget,attrs); + }, + hour_options: function(widget, attrs) + { + var options = []; + var timeformat = egw.preference('common','timeformat'); + for (var h = 0; h <= 23; ++h) + { + options.push({ + value: h, + label: timeformat == 12 ? + (( 12 ? h % 12 : 12)+' '+(h < 12 ? egw.lang('am') : egw.lang('pm'))) : + sprintf('%02d',h) + }); + } + return options; + }, + app_options: function(widget,attrs) { + var options = ','+(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + cat_options: function(widget, attrs) { + // Add in application, if not there + if(typeof attrs.other == 'undefined') + { + attrs.other = new Array(4); + } + if(typeof attrs.other[3] == 'undefined') + { + attrs.other[3] = attrs.application || widget.getInstanceManager().app; + } + var options =(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + country_options: function(widget, attrs) { + var options = ','+(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + dow_options: function(widget,attrs) { + var options = ','+(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + lang_options: function(widget,attrs) { + var options = ','+(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + timezone_options: function(widget,attrs) { + var options = ','+(attrs.other||[]).join(','); + return this.cached_server_side_options(widget, options, attrs); + }, + /** + * Some options change, or are too complicated to have twice, so we get the + * options from the server once, then keep them to use if they're needed again. + * We use the options string to keep the different possibilites (eg. categories + * for different apps) seperate. + * + * @param {et2_selectbox} widget Selectbox we're looking at + * @param {string} options_string + * @param {Object} attrs Widget attributes (not yet fully set) + * @returns {Object} Array of options, or empty and they'll get filled in later + */ + cached_server_side_options: function(widget, options_string, attrs) + { + var cache_id = widget._type+'_'+options_string; + var cache = egw.window.et2_selectbox.type_cache[cache_id]; + if (typeof cache == 'undefined') + { + // Fetch with json instead of jsonq because there may be more than + // one widget listening for the response by the time it gets back, + // and we can't do that when it's queued. + egw.window.et2_selectbox.type_cache[cache_id] = egw.json( + widget.getInstanceManager().app+'.etemplate_widget_menupopup.ajax_get_options.etemplate', + [widget._type,options_string] + ).sendRequest(); + } + cache = egw.window.et2_selectbox.type_cache[cache_id]; + if(typeof cache.done == 'function') + { + // pending, wait for it + cache.done(jQuery.proxy(function(response) { + egw.window.et2_selectbox.type_cache[this.cache_id] = response.response[0].data||undefined; + // Set select_options in attributes in case we get a resonse before + // the widget is finished loading (otherwise it will re-set to {}) + attrs.select_options = egw.window.et2_selectbox.type_cache[this.cache_id]; + + egw.window.setTimeout(jQuery.proxy(function() { + this.widget.set_select_options(egw.window.et2_selectbox.type_cache[this.cache_id]||{}); + },this),1); + },{widget:widget,cache_id:cache_id})); + return {}; + } + else + { + return cache; + } } });