Starting on selectboxes with static options.

Not entirely sure this is the way to go, but at least we don't have to
 1. copy the options
 2. inherit the whole editable object for a readonly
This commit is contained in:
nathan 2022-01-13 15:28:52 -07:00
parent 55b4b28f29
commit 20c82b6d72
4 changed files with 476 additions and 131 deletions

View File

@ -13,6 +13,7 @@ import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {et2_readAttrWithDefault} from "../et2_core_xml"; import {et2_readAttrWithDefault} from "../et2_core_xml";
import {css, html, render, repeat, TemplateResult} from "@lion/core"; import {css, html, render, repeat, TemplateResult} from "@lion/core";
import {cssImage} from "../Et2Widget/Et2Widget"; import {cssImage} from "../Et2Widget/Et2Widget";
import {StaticOptions} from "./StaticOptions";
export interface SelectOption export interface SelectOption
{ {
@ -23,51 +24,21 @@ export interface SelectOption
} }
/** /**
* Base class for things that do selectbox type behaviour, to avoid putting too much or copying into read-only
* selectboxes, also for common handling of properties for more special selectboxes.
*
* LionSelect (and any other LionField) use slots to wrap a real DOM node. ET2 doesn't expect this, * LionSelect (and any other LionField) use slots to wrap a real DOM node. ET2 doesn't expect this,
* so we have to create the input node (via slots()) and respect that it is _external_ to the Web Component. * so we have to create the input node (via slots()) and respect that it is _external_ to the Web Component.
* This complicates things like adding the options, since we can't just override _inputGroupInputTemplate() * This complicates things like adding the options, since we can't just override _inputGroupInputTemplate()
* and include them when rendering - the parent expects to find the <select> added via a slot, render() would * and include them when rendering - the parent expects to find the <select> added via a slot, render() would
* put it inside the shadowDOM. That's fine, but then it doesn't get created until render(), and the parent * put it inside the shadowDOM. That's fine, but then it doesn't get created until render(), and the parent
* (LionField) can't find it when it looks for it before then. * (LionField) can't find it when it looks for it before then.
*
*/ */
export class Et2Select extends Et2InputWidget(LionSelect) export class Et2WidgetWithSelect extends Et2InputWidget(LionSelect)
{ {
protected _options : SelectOption[] = []; protected _options : SelectOption[] = [];
static get styles()
{
return [
...super.styles,
css`
:host {
display: inline-block;
}
select {
color: var(--input-text-color, #26537c);
border-radius: 3px;
flex: 1 0 auto;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 20px;
border-width: 1px;
border-style: solid;
border-color: #e6e6e6;
-webkit-appearance: none;
-moz-appearance: none;
margin: 0;
background: #fff no-repeat center right;
background-image: ${cssImage('arrow_down')};
background-size: 8px auto;
background-position-x: calc(100% - 8px);
text-indent: 5px;
}
select:hover {
box-shadow: 1px 1px 1px rgb(0 0 0 / 60%);
}`
];
}
static get properties() static get properties()
{ {
return { return {
@ -77,7 +48,13 @@ export class Et2Select extends Et2InputWidget(LionSelect)
*/ */
empty_label: String, empty_label: String,
select_options: Object /**
* Select box options
*
* Will be found automatically based on ID and type, or can be set explicitly in the template using
* <option/> children, or using widget.set_select_options(SelectOption[])
*/
select_options: Object,
} }
} }
@ -86,20 +63,6 @@ export class Et2Select extends Et2InputWidget(LionSelect)
super(); super();
} }
connectedCallback()
{
super.connectedCallback();
// Add in actual options as children to select, if not already there
if(this._inputNode.children.length == 0)
{
render(html`${this._emptyLabelTemplate()}
${repeat(this.get_select_options(), (option : SelectOption) => option.value, this._optionTemplate)}`,
this._inputNode
);
}
}
/** /**
* Set the ID of the widget * Set the ID of the widget
* *
@ -154,14 +117,6 @@ export class Et2Select extends Et2InputWidget(LionSelect)
{ {
this._options = new_options; this._options = new_options;
} }
// Add in actual options as children to select
if(this._inputNode)
{
render(html`${this._emptyLabelTemplate()}
${repeat(this.get_select_options(), (option : SelectOption) => option.value, this._optionTemplate)}`,
this._inputNode
);
}
} }
get_select_options() get_select_options()
@ -169,31 +124,14 @@ export class Et2Select extends Et2InputWidget(LionSelect)
return this._options; return this._options;
} }
get slots()
{
return {
...super.slots,
input: () =>
{
return document.createElement("select");
}
}
}
_emptyLabelTemplate() : TemplateResult _emptyLabelTemplate() : TemplateResult
{ {
if(!this.empty_label) return html`${this.empty_label}`;
{
return html``;
}
return html`
<option value="">${this.empty_label}</option>`;
} }
_optionTemplate(option : SelectOption) : TemplateResult _optionTemplate(option : SelectOption) : TemplateResult
{ {
return html` return html``;
<option value="${option.value}" title="${option.title}">${option.label}</option>`;
} }
loadFromXML(_node : Element) loadFromXML(_node : Element)
@ -242,57 +180,122 @@ export class Et2Select extends Et2InputWidget(LionSelect)
this.set_select_options(sel_options); this.set_select_options(sel_options);
} }
} }
} }
export class Et2Select extends Et2WidgetWithSelect
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: inline-block;
width: 100%;
}
select {
width: 100%
color: var(--input-text-color, #26537c);
border-radius: 3px;
flex: 1 0 auto;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 20px;
border-width: 1px;
border-style: solid;
border-color: #e6e6e6;
-webkit-appearance: none;
-moz-appearance: none;
margin: 0;
background: #fff no-repeat center right;
background-image: ${cssImage('arrow_down')};
background-size: 8px auto;
background-position-x: calc(100% - 8px);
text-indent: 5px;
}
select:hover {
box-shadow: 1px 1px 1px rgb(0 0 0 / 60%);
}`
];
}
get slots()
{
return {
...super.slots,
input: () =>
{
return document.createElement("select");
}
}
}
connectedCallback()
{
super.connectedCallback();
// Add in actual options as children to select, if not already there
if(this._inputNode.children.length == 0)
{
render(html`${this._emptyLabelTemplate()}
${repeat(this.get_select_options(), (option : SelectOption) => option.value, this._optionTemplate)}`,
this._inputNode
);
}
}
set_select_options(new_options : SelectOption[] | { [p : string] : string })
{
super.set_select_options(new_options);
// Add in actual options as children to select
if(this._inputNode)
{
render(html`${this._emptyLabelTemplate()}
${repeat(this.get_select_options(), (option : SelectOption) => option.value, this._optionTemplate)}`,
this._inputNode
);
}
}
_emptyLabelTemplate() : TemplateResult
{
if(!this.empty_label)
{
return html``;
}
return html`
<option value="">${this.empty_label}</option>`;
}
_optionTemplate(option : SelectOption) : TemplateResult
{
return html`
<option value="${option.value}" title="${option.title}">${option.label}</option>`;
}
}
/**
* Use a single StaticOptions, since it should have no state
* @type {StaticOptions}
*/
const so = new StaticOptions();
/** /**
* Find the select options for a widget, out of the many places they could be. * Find the select options for a widget, out of the many places they could be.
* @param {Et2Widget} widget to check for. Should be some sort of select widget. * @param {Et2Widget} widget to check for. Should be some sort of select widget.
* @param {object} attr_options Select options in attributes array * @param {object} attr_options Select options in attributes array
* @param {object} attrs Widget attributes * @param {object} attrs Widget attributes
* @return {object} Select options, or empty object * @return {SelectOption[]} Select options, or empty array
*/ */
export function find_select_options(widget, attr_options?, attrs?) export function find_select_options(widget, attr_options?, attrs = {}) : SelectOption[]
{ {
let name_parts = widget.id.replace(/&#x5B;/g, '[').replace(/]|&#x5D;/g, '').split('['); let name_parts = widget.id.replace(/&#x5B;/g, '[').replace(/]|&#x5D;/g, '').split('[');
let type_options : SelectOption[] = []; let type_options : SelectOption[] = [];
let content_options : SelectOption[] = []; let content_options : SelectOption[] = [];
// First check type, there may be static options.
// TODO
/*
var type = widget._type;
var type_function = type.replace('select-', '').replace('taglist-', '').replace('_ro', '') + '_options';
if(typeof this[type_function] == 'function')
{
var old_type = widget._type;
widget._type = type.replace('taglist-', 'select-');
if(typeof attrs.other == 'string')
{
attrs.other = attrs.other.split(',');
}
// Copy, to avoid accidental modification
//
// type options used to use jQuery.extend deep copy to get a clone object of options
// but as jQuery.extend deep copy is very expensive operation in MSIE (in this case almost 400ms)
// we use JSON parsing instead to copy the options object
type_options = this[type_function].call(this, widget, attrs);
try
{
type_options = JSON.parse(JSON.stringify(type_options));
}
catch(e)
{
egw.debug(e);
}
widget._type = old_type;
}
*/
// Try to find the options inside the "sel-options" // Try to find the options inside the "sel-options"
if(widget.getArrayMgr("sel_options")) if(widget.getArrayMgr("sel_options"))
{ {
@ -444,4 +447,26 @@ export function find_select_options(widget, attr_options?, attrs?)
} }
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement // @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select", Et2Select); customElements.define("et2-select", Et2Select);
export class Et2SelectBool extends Et2Select
{
get_select_options() : SelectOption[]
{
return so.bool(this);
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select-bool", Et2SelectBool);
export class Et2SelectPercent extends Et2Select
{
get_select_options() : SelectOption[]
{
return so.percent(this, {});
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select-percent", Et2SelectPercent);

View File

@ -12,6 +12,9 @@ import {html, LitElement} from "@lion/core";
import {et2_IDetachedDOM} from "../et2_core_interfaces"; import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {Et2Widget} from "../Et2Widget/Et2Widget"; import {Et2Widget} from "../Et2Widget/Et2Widget";
import {find_select_options, SelectOption} from "./Et2Select"; import {find_select_options, SelectOption} from "./Et2Select";
import {StaticOptions} from "./StaticOptions";
const so = new StaticOptions();
/** /**
* This is a stripped-down read-only widget used in nextmatch * This is a stripped-down read-only widget used in nextmatch
@ -34,13 +37,27 @@ export class Et2SelectReadonly extends Et2Widget(LitElement) implements et2_IDet
return { return {
...super.properties, ...super.properties,
value: String, value: String,
select_options: Object select_options: Object,
/**
* Specific sub-type of select. Most are just static lists (percent, boolean)
*/
type: {type: String}
} }
} }
constructor() constructor()
{ {
super(); super();
this.type = "";
}
protected find_select_options(_attrs)
{
let sel_options = find_select_options(this, _attrs['select_options'], _attrs);
if(sel_options.length > 0)
{
this.set_select_options(sel_options);
}
} }
transformAttributes(_attrs) transformAttributes(_attrs)
@ -59,11 +76,7 @@ export class Et2SelectReadonly extends Et2Widget(LitElement) implements et2_IDet
super.transformAttributes(_attrs); super.transformAttributes(_attrs);
let sel_options = find_select_options(this, _attrs['select_options'], _attrs); this.find_select_options(_attrs)
if(sel_options.length > 0)
{
this.set_select_options(sel_options);
}
} }
set_value(value) set_value(value)
@ -124,4 +137,33 @@ export class Et2SelectReadonly extends Et2Widget(LitElement) implements et2_IDet
} }
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement // @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select_ro", Et2SelectReadonly); customElements.define("et2-select_ro", Et2SelectReadonly);
export class Et2SelectBoolReadonly extends Et2SelectReadonly
{
constructor()
{
super();
this._options = so.bool(this);
}
protected find_select_options(_attrs) {}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select-bool_ro", Et2SelectBoolReadonly);
export class Et2SelectPercentReadonly extends Et2SelectReadonly
{
constructor()
{
super();
this._options = so.percent(this, {});
}
protected find_select_options(_attrs) {}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-select-percent_ro", Et2SelectPercentReadonly);

View File

@ -0,0 +1,282 @@
/**
* 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\Select::typeOptions()
* The type specific legacy options wind up in attrs.other, but should be explicitly
* defined and set.
*
* @param {type} widget
*/
import {
Et2WidgetWithSelect, find_select_options, SelectOption
} from "./Et2Select";
import {sprintf} from "../../egw_action/egw_action_common";
import {Et2SelectReadonly} from "./Et2SelectReadonly";
/**
* 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 possibilities (eg. categories
* for different apps) separate.
*
* @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
*/
export class StaticOptions
{
cached_server_side(widget : Et2WidgetWithSelect | Et2SelectReadonly, options_string, attrs) : SelectOption[]
{
// normalize options by removing trailing commas
options_string = options_string.replace(/,+$/, '');
const cache_id = widget._type + '_' + options_string;
const cache_owner = widget.egw().getCache('Et2Select');
let cache = cache_owner[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.
const req = egw.json(
'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options',
[widget.type, options_string, attrs.value]
).sendRequest();
if(typeof cache === 'undefined')
{
cache_owner[cache_id] = req;
}
cache = req;
}
if(typeof cache.then === 'function')
{
// pending, wait for it
cache.then((response) =>
{
cache = cache_owner[cache_id] = response.response[0].data || undefined;
// Set select_options in attributes in case we get a response before
// the widget is finished loading (otherwise it will re-set to {})
attrs.select_options = cache;
// Avoid errors if widget is destroyed before the timeout
if(widget && typeof widget.id !== 'undefined')
{
widget.set_select_options(find_select_options(widget, {}, widget.options));
}
});
return [];
}
else
{
// Check that the value is in there
// Make sure we are not requesting server for an empty value option or
// other widgets but select-timezone as server won't find anything and
// it will fall into an infinitive loop, e.g. select-cat widget.
if(attrs.value && attrs.value != "" && attrs.value != "0" && attrs.type == "select-timezone")
{
var missing_option = true;
for(var i = 0; i < cache.length && missing_option; i++)
{
if(cache[i].value == attrs.value)
{
missing_option = false;
}
}
// Try again - ask the server with the current value this time
if(missing_option)
{
delete cache_owner[cache_id];
return this.cached_server_side(widget, options_string, attrs);
}
else
{
if(attrs.value && widget && widget.get_value() !== attrs.value)
{
egw.window.setTimeout(jQuery.proxy(function()
{
// Avoid errors if widget is destroyed before the timeout
if(this.widget && typeof this.widget.id !== 'undefined')
{
this.widget.set_value(this.widget.options.value);
}
}, {widget: widget}), 1);
}
}
}
return cache;
}
}
priority(widget : Et2WidgetWithSelect | Et2SelectReadonly) : SelectOption[]
{
return [
{value: "1", label: 'low'},
{value: "2", label: 'normal'},
{value: "3", label: 'high'},
{value: "0", label: 'undefined'}
];
}
bool(widget : Et2WidgetWithSelect | Et2SelectReadonly) : SelectOption[]
{
return [
{value: "0", label: 'no'},
{value: "1", label: 'yes'}
];
}
month(widget : Et2WidgetWithSelect | Et2SelectReadonly) : SelectOption[]
{
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(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
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(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
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(widget, attrs);
}
year(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
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(widget, attrs);
}
day(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
attrs.other = [1, 31, 1];
return this.number(widget, attrs);
}
hour(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = [];
var timeformat = widget.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(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, options, attrs);
}
cat(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
// 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(widget, options, attrs);
}
country(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = ',';
return this.cached_server_side(widget, options, attrs);
}
state(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = attrs.country_code ? attrs.country_code : 'de';
return this.cached_server_side(widget, options, attrs);
}
dow(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, options, attrs);
}
lang(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, options, attrs);
}
timezone(widget : Et2WidgetWithSelect | Et2SelectReadonly, attrs) : SelectOption[]
{
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, options, attrs);
}
}

View File

@ -781,12 +781,16 @@ export class et2_widget extends ClassWithAttributes
// Get the constructor - if the widget is readonly, use the special "_ro" // Get the constructor - if the widget is readonly, use the special "_ro"
// constructor if it is available // constructor if it is available
var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName]; var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName];
if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") if(readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined")
{ {
constructor = et2_registry[_nodeName + "_ro"]; constructor = et2_registry[_nodeName + "_ro"];
} }
if (undefined == window.customElements.get(_nodeName)) if(typeof window.customElements.get(_node.nodeName.toLowerCase()) !== "undefined")
{
widget = loadWebComponent(_node.nodeName.toLowerCase(), _node, this);
}
else
{ {
// Parse the attributes from the given XML attributes object // Parse the attributes from the given XML attributes object
this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
@ -801,14 +805,6 @@ export class et2_widget extends ClassWithAttributes
// Load the widget itself from XML // Load the widget itself from XML
widget.loadFromXML(_node); widget.loadFromXML(_node);
} }
else
{
if(readonly === true && typeof window.customElements.get(_nodeName + "_ro") != "undefined")
{
_nodeName += "_ro";
}
widget = loadWebComponent(_nodeName, _node, this);
}
return widget; return widget;
} }