2022-03-01 12:35:33 +01:00
|
|
|
/**
|
|
|
|
* EGroupware eTemplate2 - WidgetWithSelectMixin
|
|
|
|
*
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
* @package api
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
* @author Nathan Gray
|
|
|
|
*/
|
|
|
|
|
2022-06-10 18:25:31 +02:00
|
|
|
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
2022-09-22 19:15:25 +02:00
|
|
|
import {html, LitElement, PropertyValues, render, TemplateResult} from "@lion/core";
|
2022-03-01 12:35:33 +01:00
|
|
|
import {et2_readAttrWithDefault} from "../et2_core_xml";
|
2022-06-01 17:23:07 +02:00
|
|
|
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
|
2022-06-10 18:25:31 +02:00
|
|
|
import {SearchMixinInterface} from "./SearchMixin";
|
2022-03-01 12:35:33 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2022-03-03 18:38:56 +01:00
|
|
|
* As with most other widgets that extend Lion components, do not override render().
|
|
|
|
* To extend this mixin, override:
|
|
|
|
* - _optionTargetNode(): Return the HTMLElement where the "options" go.
|
|
|
|
* - _optionTemplate(option:SelectOption): Renders the option. To use a special widget, use its tag in render.
|
|
|
|
* Select option:
|
|
|
|
* ```js
|
|
|
|
* return html`
|
|
|
|
* <option value="${option.value}" title="${option.title}" ?selected=${option.value == this.modelValue}>
|
|
|
|
* ${option.label}
|
|
|
|
* </option>`;
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* or pass it off to a different WebComponent:
|
|
|
|
*
|
|
|
|
* ```js
|
|
|
|
* _optionTemplate(option:SelectOption) : TemplateResult
|
|
|
|
* {
|
|
|
|
* return html`
|
|
|
|
* <special-option-tag .value=${option}></special-option-tag>`;
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Optionally, you can override:
|
|
|
|
* - _emptyLabelTemplate(): How to render the empty label
|
|
|
|
* - slots(): Most Lion components have an input slot where the <input> tag is created.
|
|
|
|
* You can specify something else, or return {} to do your own thing. This is a little more complicated. You should
|
|
|
|
* also override _inputGroupInputTemplate() to do what you normally would in render().
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Technical note:
|
2022-03-01 12:35:33 +01:00
|
|
|
* 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.
|
|
|
|
* 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
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
*/
|
2022-06-10 18:25:31 +02:00
|
|
|
// Export the Interface for TypeScript
|
|
|
|
type Constructor<T = {}> = new (...args : any[]) => T;
|
|
|
|
|
|
|
|
export const Et2widgetWithSelectMixin = <T extends Constructor<LitElement>>(superclass : T) =>
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
|
|
|
class Et2WidgetWithSelect extends Et2InputWidget(superclass)
|
|
|
|
{
|
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
/**
|
|
|
|
* Textual label for first row, eg: 'All' or 'None'. It's value will be ''
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
emptyLabel: String,
|
2022-03-01 12:35:33 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Select box options
|
|
|
|
*
|
|
|
|
* Will be found automatically based on ID and type, or can be set explicitly in the template using
|
2022-03-16 21:36:23 +01:00
|
|
|
* <option/> children, or using widget.select_options = SelectOption[]
|
2022-03-01 12:35:33 +01:00
|
|
|
*/
|
2022-07-04 23:08:00 +02:00
|
|
|
select_options: {type: Object, noAccessor: true},
|
2022-07-12 01:27:17 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Limit size
|
|
|
|
*/
|
|
|
|
rows: {type: Number, noAccessor: true, reflect: true}
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-08 22:57:26 +02:00
|
|
|
/**
|
|
|
|
* Options found in the XML when reading the template
|
|
|
|
* @type {SelectOption[]}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private _xmlOptions : SelectOption[] = [];
|
|
|
|
|
2022-06-10 18:25:31 +02:00
|
|
|
constructor(...args : any[])
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
2022-06-10 18:25:31 +02:00
|
|
|
super(...args);
|
2022-03-01 12:35:33 +01:00
|
|
|
|
2022-06-08 22:57:26 +02:00
|
|
|
this.__select_options = <SelectOption[]>[];
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @param {import('@lion/core').PropertyValues } changedProperties */
|
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
|
|
|
|
2022-11-07 17:56:26 +01:00
|
|
|
// If the ID changed (or was just set) and select_options wasn't, find the new select options
|
|
|
|
if(changedProperties.has("id") && !changedProperties.has("select_options"))
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
2022-06-08 22:57:26 +02:00
|
|
|
const options = find_select_options(this, {}, this._xmlOptions);
|
2022-11-07 17:56:26 +01:00
|
|
|
if(options.length)
|
|
|
|
{
|
|
|
|
this.select_options = options;
|
|
|
|
}
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
2022-03-03 18:38:56 +01:00
|
|
|
|
|
|
|
// Add in actual option tags to the DOM based on the new select_options
|
2022-07-21 17:57:50 +02:00
|
|
|
if(changedProperties.has('select_options') || changedProperties.has("emptyLabel"))
|
2022-03-03 18:38:56 +01:00
|
|
|
{
|
|
|
|
// Add in options as children to the target node
|
2022-08-15 18:30:30 +02:00
|
|
|
this._renderOptions();
|
2022-09-15 16:30:50 +02:00
|
|
|
|
|
|
|
// This is needed to display initial load value in some cases, like infolog nm header filters
|
2022-11-07 17:56:26 +01:00
|
|
|
if(this.handleMenuSlotChange && !this.hasUpdated)
|
2022-08-05 19:32:59 +02:00
|
|
|
{
|
|
|
|
this.handleMenuSlotChange();
|
|
|
|
}
|
2022-03-03 18:38:56 +01:00
|
|
|
}
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
2022-08-15 18:30:30 +02:00
|
|
|
/**
|
|
|
|
* Render select_options as child DOM Nodes
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _renderOptions()
|
|
|
|
{
|
|
|
|
// Add in options as children to the target node
|
2022-09-22 19:15:25 +02:00
|
|
|
if(!this._optionTargetNode)
|
2022-08-15 18:30:30 +02:00
|
|
|
{
|
2022-09-22 19:15:25 +02:00
|
|
|
return Promise.resolve();
|
2022-08-15 18:30:30 +02:00
|
|
|
}
|
2022-09-22 19:15:25 +02:00
|
|
|
/**
|
|
|
|
* Doing all this garbage to get the options to always show up.
|
|
|
|
* If we just do `render(options, target)`, they only show up in the DOM the first time. If the
|
|
|
|
* same option comes back in a subsequent search, map() does not put it into the DOM.
|
|
|
|
* If we render into a new target, the options get rendered, but we have to wait for them to be
|
|
|
|
* rendered before we can do anything else with them.
|
|
|
|
*/
|
|
|
|
let temp_target = document.createElement("div");
|
|
|
|
|
2022-10-19 00:06:28 +02:00
|
|
|
let options = html`${this._emptyLabelTemplate()}${this.select_options
|
|
|
|
// Filter out empty values if we have empty label to avoid duplicates
|
|
|
|
.filter(o => this.emptyLabel ? o.value !== '' : o)
|
|
|
|
.map(this._optionTemplate.bind(this))}`;
|
2022-09-22 19:15:25 +02:00
|
|
|
|
|
|
|
render(options, temp_target);
|
|
|
|
return Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
|
|
|
|
.then(() =>
|
|
|
|
{
|
2022-10-07 23:31:06 +02:00
|
|
|
this._optionTargetNode.replaceChildren(
|
|
|
|
...Array.from(temp_target.querySelectorAll(":scope > *")),
|
|
|
|
...Array.from(this._optionTargetNode.querySelectorAll(":scope > [slot]"))
|
|
|
|
);
|
2022-09-22 19:15:25 +02:00
|
|
|
if(typeof this.handleMenuSlotChange == "function")
|
|
|
|
{
|
|
|
|
this.handleMenuSlotChange();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-08-15 18:30:30 +02:00
|
|
|
}
|
|
|
|
|
2022-03-01 15:51:40 +01:00
|
|
|
/**
|
|
|
|
* Overwritten as sometimes called before this._inputNode is available
|
|
|
|
*
|
|
|
|
* @param {*} v - modelValue: can be an Object, Number, String depending on the
|
|
|
|
* input type(date, number, email etc)
|
|
|
|
* @returns {string} formattedValue
|
|
|
|
*/
|
|
|
|
formatter(v)
|
|
|
|
{
|
|
|
|
if (!this._inputNode)
|
|
|
|
{
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
return super.formatter(v);
|
|
|
|
}
|
|
|
|
|
2022-03-01 12:35:33 +01:00
|
|
|
/**
|
|
|
|
* Set the select options
|
|
|
|
*
|
2022-03-16 21:36:23 +01:00
|
|
|
* @param new_options
|
2022-03-01 12:35:33 +01:00
|
|
|
*/
|
2022-05-13 17:01:44 +02:00
|
|
|
set select_options(new_options : SelectOption[])
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
const old_options = this.__select_options;
|
2022-06-01 17:23:07 +02:00
|
|
|
|
|
|
|
this.__select_options = cleanSelectOptions(new_options);
|
|
|
|
|
2022-03-16 21:36:23 +01:00
|
|
|
this.requestUpdate("select_options", old_options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set select options
|
|
|
|
*
|
|
|
|
* @deprecated assign to select_options
|
|
|
|
* @param new_options
|
|
|
|
*/
|
2022-05-13 17:01:44 +02:00
|
|
|
set_select_options(new_options : SelectOption[] | { [key : string] : string }[])
|
2022-03-16 21:36:23 +01:00
|
|
|
{
|
2022-05-13 17:01:44 +02:00
|
|
|
this.select_options = <SelectOption[]>new_options;
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
2022-05-13 17:01:44 +02:00
|
|
|
get select_options() : SelectOption[]
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
return this.__select_options;
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
2022-03-03 18:38:56 +01:00
|
|
|
/**
|
|
|
|
* Get the node where we're putting the options
|
|
|
|
*
|
|
|
|
* If this were a normal selectbox, this would be just the <select> tag (this._inputNode) but in a more
|
|
|
|
* complicated widget, this could be anything.
|
|
|
|
*
|
|
|
|
* @overridable
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
*/
|
|
|
|
get _optionTargetNode() : HTMLElement
|
|
|
|
{
|
|
|
|
return <HTMLElement><unknown>this;
|
|
|
|
}
|
|
|
|
|
2022-03-01 12:35:33 +01:00
|
|
|
/**
|
|
|
|
* Render the "empty label", used when the selectbox does not currently have a value
|
|
|
|
*
|
2022-03-03 18:38:56 +01:00
|
|
|
* @overridable
|
|
|
|
* @returns {TemplateResult}
|
2022-03-01 12:35:33 +01:00
|
|
|
*/
|
|
|
|
_emptyLabelTemplate() : TemplateResult
|
|
|
|
{
|
2022-07-26 15:46:42 +02:00
|
|
|
return html`${this.emptyLabel}`;
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
2022-03-03 18:38:56 +01:00
|
|
|
/**
|
|
|
|
* Render a single option
|
|
|
|
*
|
|
|
|
* Override this method to specify how to render each option.
|
|
|
|
* In a normal selectbox, this would be something like:
|
|
|
|
*```
|
|
|
|
* <option value="${option.value}" title="${option.title}" ?selected=${option.value == this.modelValue}>
|
|
|
|
* ${option.label}
|
|
|
|
* </option>`;
|
|
|
|
* ```
|
|
|
|
* but you can do whatever you need. To use a different WebComponent, just use its tag instead of "option".
|
|
|
|
* We should even be able to pass the whole SelectOption across
|
|
|
|
* ```
|
|
|
|
* <special-option .value=${option}></special-option>
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @overridable
|
|
|
|
* @param {SelectOption} option
|
|
|
|
* @returns {TemplateResult}
|
|
|
|
*/
|
2022-03-01 12:35:33 +01:00
|
|
|
_optionTemplate(option : SelectOption) : TemplateResult
|
|
|
|
{
|
2022-03-03 18:38:56 +01:00
|
|
|
return html`
|
|
|
|
<span>Override _optionTemplate(). ${option.value} => ${option.label}</span>`;
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load extra stuff from the template node. In particular, we're looking for any <option/> tags added.
|
|
|
|
*
|
|
|
|
* @param {Element} _node
|
|
|
|
*/
|
|
|
|
loadFromXML(_node : Element)
|
|
|
|
{
|
|
|
|
let new_options = [];
|
2022-05-31 17:58:49 +02:00
|
|
|
|
|
|
|
// Read the option-tags, but if not rendered there won't be any yet so check existing options
|
|
|
|
let options = _node.querySelectorAll("option");
|
2022-03-01 12:35:33 +01:00
|
|
|
for(let i = 0; i < options.length; i++)
|
|
|
|
{
|
|
|
|
new_options.push({
|
|
|
|
value: et2_readAttrWithDefault(options[i], "value", options[i].textContent),
|
|
|
|
// allow options to contain multiple translated sub-strings eg: {Firstname}.{Lastname}
|
2022-06-17 22:42:42 +02:00
|
|
|
label: options[i].textContent.replace(/{([^}]+)}/g, (str, p1) =>
|
2022-03-01 12:35:33 +01:00
|
|
|
{
|
|
|
|
return this.egw().lang(p1);
|
|
|
|
}),
|
|
|
|
title: et2_readAttrWithDefault(options[i], "title", "")
|
|
|
|
});
|
|
|
|
}
|
2022-06-08 22:57:26 +02:00
|
|
|
this._xmlOptions = new_options;
|
2022-05-31 17:58:49 +02:00
|
|
|
if(options.length == 0 && this.__select_options.length)
|
|
|
|
{
|
|
|
|
// Start with any existing options, (static options from type)
|
|
|
|
// Use a copy since we'll probably be modifying it, and we don't want to change for any other
|
|
|
|
// widget of the same static type
|
|
|
|
new_options = [...this.__select_options];
|
|
|
|
}
|
2022-03-01 12:35:33 +01:00
|
|
|
|
|
|
|
if(this.id)
|
|
|
|
{
|
|
|
|
new_options = find_select_options(this, {}, new_options);
|
|
|
|
}
|
2022-05-31 17:58:49 +02:00
|
|
|
if(new_options.length)
|
2022-03-16 21:36:23 +01:00
|
|
|
{
|
|
|
|
this.select_options = new_options;
|
|
|
|
}
|
2022-03-01 12:35:33 +01:00
|
|
|
}
|
|
|
|
}
|
2022-06-10 18:25:31 +02:00
|
|
|
|
|
|
|
return Et2WidgetWithSelect as unknown as Constructor<SearchMixinInterface> & Et2InputWidgetInterface & T;
|
|
|
|
}
|