Selectbox options kept on the client side

Static options copied to the client side in the JS code where possible, and requested from the server once via AJAX when needed.
This commit is contained in:
Nathan Gray 2015-04-14 18:58:21 +00:00
parent 7926f01a9a
commit 605b54183d
4 changed files with 368 additions and 48 deletions

View File

@ -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)
@ -231,36 +270,44 @@ 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
$options = (isset(self::$request->sel_options[$form_name]) ? $form_name : $this->id);
if(is_array(self::$request->sel_options[$options]))
{
if(in_array($this->attrs['type'], self::$cached_types) && !isset($form_names_done[$options]))
{
// Fix any custom options from application
self::fix_encoded_options(self::$request->sel_options[$options],true);
$form_names_done[$options] = true;
}
// Turn on search, if there's a lot of rows (unless explicitly set)
if(!array_key_exists('search',$this->attrs) && count(self::$request->sel_options[$options]) >= self::SEARCH_ROW_LIMIT)
{
@ -430,15 +477,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)
@ -465,14 +513,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)
{
@ -789,6 +829,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'));

View File

@ -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'];
@ -1216,6 +1220,11 @@ class etemplate_widget_nextmatch_customfilter extends etemplate_widget_transform
parent::beforeSendToClient($cname, $expand);
$this->setElementAttribute($form_name, 'type', $this->attrs['type']);
if($widget_type)
{
$this->setElementAttribute($form_name, 'widget_type', $widget_type);
}
parent::beforeSendToClient($cname, $expand);
}
}

View File

@ -2395,7 +2395,7 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader,
}
// Legacy: Add in 'All' option for cat_id, if not provided.
if(name == 'cat_id' && options != null && typeof options[''] == 'undefined' && typeof options[0] == 'undefined')
if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != ''))
{
widget_options.empty_label = this.egw().lang('All');
this.egw().debug('warn', 'Nextmatch category filter had no "All" option. Added, but you should fix that.');
@ -3227,12 +3227,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": {}
}
},
@ -3248,7 +3250,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)
{
@ -3256,11 +3257,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

View File

@ -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;
@ -495,7 +495,9 @@ var et2_selectbox = et2_inputWidget.extend(
}
else
{
this.egw().debug("warn", "Tried to set value '%s' that isn't an option", _value, this);
var debug_value = _value;
if(debug_value === null) debug_value == 'NULL';
this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this);
}
return;
}
@ -527,7 +529,9 @@ var et2_selectbox = et2_inputWidget.extend(
{
if(jQuery("input[value='"+_value+"']", this.multiOptions).prop("checked", true).length == 0)
{
this.egw().debug("warn", "Tried to set value that isn't an option", this, _value);
var debug_value = _value;
if(debug_value === null) debug_value == 'NULL';
this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this);
}
}
@ -749,7 +753,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' ]);
@ -757,19 +761,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 type_options = [];
var content_options = {};
// Try to find the options inside the "sel-options" array
// 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;
type_options = this[type_function].call(this, widget, attrs);
widget._type = old_type;
}
// Try to find the options inside the "sel-options"
if(widget.getArrayMgr("sel_options"))
{
// Try first according to ID
@ -860,7 +880,226 @@ jQuery.extend(et2_selectbox,
{
content_options = {};
}
// Include type options, preferring any content options
if(type_options.length || !jQuery.isEmptyObject(type_options))
{
for(var i in content_options)
{
var value = typeof content_options[i] == 'object' && typeof content_options[i].value !== 'undefined' ? content_options[i].value : i;
var added = false;
// Override any existing
for(var j in type_options)
{
if(type_options[j].value === value)
{
added = true;
type_options[j] = content_options[i];
break;
}
}
if(!added)
{
type_options.splice(i,0,content_options[i]);
}
}
content_options = type_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(et2_selectbox.find_select_options(this.widget,{}, this.widget.options));
},this),1);
},{widget:widget,cache_id:cache_id}));
return [];
}
else
{
return cache;
}
}
});