2022-01-04 23:38:10 +01:00
|
|
|
/**
|
2022-03-01 12:35:33 +01:00
|
|
|
* EGroupware eTemplate2 - Select WebComponent
|
2022-01-04 23:38:10 +01:00
|
|
|
*
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
* @package api
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
* @author Nathan Gray
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2022-03-03 18:38:56 +01:00
|
|
|
import {css, html, PropertyValues, TemplateResult} from "@lion/core";
|
2022-07-15 20:56:24 +02:00
|
|
|
import {Et2StaticSelectMixin, StaticOptions} from "./StaticOptions";
|
2022-03-01 12:35:33 +01:00
|
|
|
import {Et2widgetWithSelectMixin} from "./Et2WidgetWithSelectMixin";
|
|
|
|
import {SelectOption} from "./FindSelectOptions";
|
2022-06-16 00:43:39 +02:00
|
|
|
import {SlMenuItem, SlSelect} from "@shoelace-style/shoelace";
|
2022-05-27 22:12:31 +02:00
|
|
|
import shoelace from "../Styles/shoelace";
|
2022-05-31 01:05:00 +02:00
|
|
|
import {Et2WithSearchMixin} from "./SearchMixin";
|
2022-06-16 00:43:39 +02:00
|
|
|
import {Et2Tag} from "./Tag/Et2Tag";
|
2022-06-29 19:49:24 +02:00
|
|
|
import {LionValidationFeedback} from "@lion/form-core";
|
2022-07-12 01:27:17 +02:00
|
|
|
import {RowLimitedMixin} from "../Layout/RowLimitedMixin";
|
2022-01-04 23:38:10 +01:00
|
|
|
|
2022-03-01 13:31:13 +01:00
|
|
|
// export Et2WidgetWithSelect which is used as type in other modules
|
2022-07-12 01:27:17 +02:00
|
|
|
export class Et2WidgetWithSelect extends RowLimitedMixin(Et2widgetWithSelectMixin(SlSelect))
|
2022-05-27 22:12:31 +02:00
|
|
|
{
|
|
|
|
};
|
2022-03-01 13:31:13 +01:00
|
|
|
|
2022-06-24 20:10:10 +02:00
|
|
|
/**
|
|
|
|
* Select widget
|
|
|
|
*
|
|
|
|
* At its most basic, you can select one option from a list provided. The list can be passed from the server in
|
|
|
|
* the sel_options array or options can be added as children in the template. Some extending classes provide specific
|
|
|
|
* options, such as Et2SelectPercent or Et2SelectCountry. All provided options will be mixed together and used.
|
|
|
|
*
|
|
|
|
* To allow selecting more than one option, use the attribute multiple="true". This will take & return an array
|
|
|
|
* as value instead of just a string.
|
|
|
|
*
|
|
|
|
* SearchMixin adds additional abilities to ALL select boxes
|
|
|
|
* @see Et2WithSearchMixin
|
|
|
|
*
|
|
|
|
* Override for extending widgets:
|
|
|
|
* # Custom display of selected value
|
|
|
|
* When selecting a single value (!multiple) you can override doLabelChange() to customise the displayed label
|
|
|
|
* @see Et2SelectCategory, which adds in the category icon
|
|
|
|
*
|
|
|
|
* # Custom option rows
|
|
|
|
* Options can have 'class' and 'icon' properties that will be used for the option
|
|
|
|
* The easiest way for further customisation to use CSS in an external file (like etemplate2.css) and ::part().
|
|
|
|
* @see Et2SelectCountry which displays flags via CSS instead of using SelectOption.icon
|
|
|
|
*
|
|
|
|
* # Custom tags
|
|
|
|
* When multiple is set, instead of a single value each selected value is shown in a tag. While it's possible to
|
|
|
|
* use CSS to some degree, we can also use a custom tag class that extends Et2Tag.
|
|
|
|
* 1. Create the extending class
|
|
|
|
* 2. Make sure it's loaded (add to etemplate2.ts)
|
|
|
|
* 3. In your extending Et2Select, override get tagTag() to return the custom tag name
|
|
|
|
*
|
|
|
|
*/
|
2022-06-10 18:25:31 +02:00
|
|
|
// @ts-ignore SlSelect styles is a single CSSResult, not an array, so TS complains
|
2022-06-30 17:39:22 +02:00
|
|
|
export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
|
2022-01-04 23:38:10 +01:00
|
|
|
{
|
2022-01-13 23:28:52 +01:00
|
|
|
static get styles()
|
|
|
|
{
|
|
|
|
return [
|
2022-06-10 18:25:31 +02:00
|
|
|
// Parent (SlSelect) returns a single cssResult, not an array
|
2022-05-27 22:12:31 +02:00
|
|
|
shoelace,
|
2022-08-04 23:50:52 +02:00
|
|
|
super.styles,
|
2022-01-13 23:28:52 +01:00
|
|
|
css`
|
|
|
|
:host {
|
2022-04-22 21:07:22 +02:00
|
|
|
display: block;
|
2022-07-07 21:18:42 +02:00
|
|
|
flex: 1 0 auto;
|
2022-06-10 22:11:57 +02:00
|
|
|
--icon-width: 20px;
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
2022-03-12 13:49:25 +01:00
|
|
|
|
2022-06-10 22:11:57 +02:00
|
|
|
|
|
|
|
::slotted(img), img {
|
|
|
|
vertical-align: middle;
|
|
|
|
}
|
|
|
|
|
2022-06-16 00:43:39 +02:00
|
|
|
/* Get rid of padding before/after options */
|
|
|
|
sl-menu::part(base) {
|
|
|
|
padding: 0px;
|
|
|
|
}
|
|
|
|
|
2022-07-26 17:12:52 +02:00
|
|
|
/* No horizontal scrollbar, even if options are long */
|
|
|
|
.dropdown__panel {
|
|
|
|
overflow-x: clip;
|
|
|
|
}
|
2022-08-23 19:07:02 +02:00
|
|
|
/* Ellipsis when too small */
|
|
|
|
.select__label {
|
|
|
|
display: block;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
/* This is usually not used due to flex, but is the basis for ellipsis calculation */
|
|
|
|
width: 10ex;
|
|
|
|
}
|
2022-01-04 23:38:10 +01:00
|
|
|
|
2022-06-16 00:43:39 +02:00
|
|
|
/** multiple=true uses tags for each value **/
|
|
|
|
/* styling for icon inside tag (not option) */
|
|
|
|
.tag_image {
|
|
|
|
margin-right: var(--sl-spacing-x-small);
|
|
|
|
}
|
|
|
|
/* Maximum height + scrollbar on tags (+ other styling) */
|
|
|
|
.select__tags {
|
2022-06-24 22:12:04 +02:00
|
|
|
margin-left: 0px;
|
2022-07-12 01:27:17 +02:00
|
|
|
max-height: initial;
|
2022-06-16 00:43:39 +02:00
|
|
|
overflow-y: auto;
|
|
|
|
gap: 0.1rem 0.5rem;
|
|
|
|
}
|
2022-07-12 01:27:17 +02:00
|
|
|
:host([rows]) .select__tags {
|
2022-07-12 18:50:11 +02:00
|
|
|
max-height: calc(var(--rows, 5) * 1.35rem);
|
2022-07-12 01:27:17 +02:00
|
|
|
}
|
2022-06-16 00:43:39 +02:00
|
|
|
/* Keep overflow tag right-aligned. It's the only sl-tag. */
|
|
|
|
.select__tags sl-tag {
|
|
|
|
margin-left: auto;
|
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
select:hover {
|
|
|
|
box-shadow: 1px 1px 1px rgb(0 0 0 / 60%);
|
|
|
|
}`
|
|
|
|
];
|
|
|
|
}
|
2022-01-04 23:38:10 +01:00
|
|
|
|
2022-05-27 23:39:51 +02:00
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
/**
|
|
|
|
* Toggle between single and multiple selection
|
|
|
|
*/
|
|
|
|
multiple: {
|
|
|
|
type: Boolean,
|
|
|
|
reflect: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-26 15:46:42 +02:00
|
|
|
/**
|
|
|
|
* List of properties that get translated
|
|
|
|
*
|
|
|
|
* @returns object
|
|
|
|
*/
|
|
|
|
static get translate()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.translate,
|
|
|
|
emptyLabel: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-13 23:28:52 +01:00
|
|
|
get slots()
|
2022-01-04 23:38:10 +01:00
|
|
|
{
|
2022-01-13 23:28:52 +01:00
|
|
|
return {
|
|
|
|
...super.slots,
|
|
|
|
input: () =>
|
|
|
|
{
|
|
|
|
return document.createElement("select");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-10 18:25:31 +02:00
|
|
|
constructor(...args : any[])
|
2022-05-27 23:39:51 +02:00
|
|
|
{
|
|
|
|
super();
|
2022-08-04 00:28:18 +02:00
|
|
|
// We want this on more often than off
|
|
|
|
this.hoist = true;
|
|
|
|
|
2022-05-27 23:39:51 +02:00
|
|
|
this._triggerChange = this._triggerChange.bind(this);
|
2022-06-14 23:14:17 +02:00
|
|
|
this._doResize = this._doResize.bind(this);
|
2022-05-27 23:39:51 +02:00
|
|
|
}
|
|
|
|
|
2022-01-13 23:28:52 +01:00
|
|
|
connectedCallback()
|
|
|
|
{
|
|
|
|
super.connectedCallback();
|
2022-05-27 23:39:51 +02:00
|
|
|
|
2022-06-23 23:55:42 +02:00
|
|
|
this.updateComplete.then(() =>
|
2022-05-27 23:39:51 +02:00
|
|
|
{
|
2022-06-07 00:41:29 +02:00
|
|
|
this.addEventListener("sl-change", this._triggerChange);
|
2022-06-14 23:14:17 +02:00
|
|
|
this.addEventListener("sl-after-show", this._doResize)
|
2022-05-27 23:39:51 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnectedCallback()
|
|
|
|
{
|
|
|
|
super.disconnectedCallback();
|
|
|
|
|
|
|
|
this.removeEventListener("sl-clear", this._triggerChange)
|
2022-06-08 22:01:00 +02:00
|
|
|
this.removeEventListener("sl-change", this._triggerChange);
|
2022-06-14 23:14:17 +02:00
|
|
|
this.removeEventListener("sl-after-show", this._doResize);
|
2022-04-01 19:12:19 +02:00
|
|
|
}
|
|
|
|
|
2022-06-10 18:25:31 +02:00
|
|
|
firstUpdated(changedProperties?)
|
2022-04-01 19:12:19 +02:00
|
|
|
{
|
|
|
|
super.firstUpdated(changedProperties);
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
|
|
|
|
2022-05-27 23:39:51 +02:00
|
|
|
_triggerChange(e)
|
|
|
|
{
|
2022-08-31 21:27:10 +02:00
|
|
|
if(super._triggerChange(e))
|
|
|
|
{
|
|
|
|
this.dispatchEvent(new Event("change"));
|
|
|
|
}
|
2022-05-27 23:39:51 +02:00
|
|
|
}
|
|
|
|
|
2022-06-14 23:14:17 +02:00
|
|
|
/**
|
|
|
|
* Change the menu sizing to allow the menu to be wider than the field width, but no smaller
|
|
|
|
*
|
|
|
|
* @param e
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private _doResize(e)
|
|
|
|
{
|
|
|
|
this.menu.style.minWidth = this.menu.style.width;
|
|
|
|
this.menu.style.width = "";
|
|
|
|
}
|
|
|
|
|
2022-03-03 18:38:56 +01:00
|
|
|
/**
|
|
|
|
* Get the node where we're putting the selection options
|
|
|
|
*
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
*/
|
|
|
|
get _optionTargetNode() : HTMLElement
|
|
|
|
{
|
2022-05-27 22:12:31 +02:00
|
|
|
return <HTMLElement><unknown>this;
|
2022-03-03 18:38:56 +01:00
|
|
|
}
|
|
|
|
|
2022-06-23 23:55:42 +02:00
|
|
|
/**
|
|
|
|
* Handle the case where there is no value set, or the value provided is not an option.
|
|
|
|
* If this happens, we choose the first option or empty label.
|
|
|
|
*
|
|
|
|
* Careful when this is called. We change the value here, so an infinite loop is possible if the widget has
|
|
|
|
* onchange.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private fix_bad_value()
|
|
|
|
{
|
2022-08-30 22:27:19 +02:00
|
|
|
if(this.multiple || (!this.emptyLabel && (!Array.isArray(this.select_options) || this.select_options.length == 0)))
|
2022-07-04 23:08:00 +02:00
|
|
|
{
|
|
|
|
// Nothing to do here
|
|
|
|
return;
|
|
|
|
}
|
2022-08-31 21:27:10 +02:00
|
|
|
// See if parent (search / free entry) is OK with it
|
|
|
|
if(super.fix_bad_value())
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2022-09-01 17:58:08 +02:00
|
|
|
// If somebody gave '' as a select_option, let it be
|
|
|
|
if(this.value === '' && this.select_options.filter((option) => this.value === option.value).length == 1)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2022-06-23 23:55:42 +02:00
|
|
|
// If no value is set, choose the first option
|
2022-07-04 23:08:00 +02:00
|
|
|
// Only do this on once during initial setup, or it can be impossible to clear the value
|
2022-06-23 23:55:42 +02:00
|
|
|
const valueArray = Array.isArray(this.value) ? this.value : (!this.value ? [] : this.value.toString().split(','));
|
2022-07-04 23:08:00 +02:00
|
|
|
|
2022-07-21 17:57:50 +02:00
|
|
|
// value not in options --> use emptyLabel, if exists, or first option otherwise
|
2022-07-04 23:08:00 +02:00
|
|
|
if(this.select_options.filter((option) => valueArray.find(val => val == option.value)).length === 0)
|
2022-06-23 23:55:42 +02:00
|
|
|
{
|
2022-07-11 19:01:42 +02:00
|
|
|
let oldValue = this.value;
|
2022-07-21 17:57:50 +02:00
|
|
|
this.value = this.emptyLabel ? "" : "" + this.select_options[0]?.value;
|
2022-06-23 23:55:42 +02:00
|
|
|
// ""+ to cast value of 0 to "0", to not replace with ""
|
2022-07-11 19:01:42 +02:00
|
|
|
this.requestUpdate("value", oldValue);
|
2022-06-23 23:55:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-12 13:49:25 +01:00
|
|
|
/**
|
|
|
|
* @deprecated use this.multiple = multi
|
|
|
|
*
|
|
|
|
* @param multi
|
|
|
|
*/
|
2022-03-10 14:48:27 +01:00
|
|
|
set_multiple(multi)
|
|
|
|
{
|
2022-03-12 13:49:25 +01:00
|
|
|
this.multiple = multi;
|
|
|
|
}
|
|
|
|
|
2022-06-18 15:06:50 +02:00
|
|
|
set_value(val : string | string[] | number | number[])
|
2022-03-14 20:13:47 +01:00
|
|
|
{
|
2022-07-19 19:47:03 +02:00
|
|
|
if(typeof val === 'string' && val.indexOf(',') !== -1)
|
|
|
|
{
|
|
|
|
val = val.split(',');
|
|
|
|
}
|
|
|
|
if(typeof val === 'number')
|
2022-06-10 13:07:27 +02:00
|
|
|
{
|
|
|
|
val = val.toString();
|
|
|
|
}
|
2022-07-19 19:47:03 +02:00
|
|
|
if(Array.isArray(val))
|
2022-06-18 15:06:50 +02:00
|
|
|
{
|
|
|
|
val = val.map(v => typeof v === 'number' ? v.toString() : v || '');
|
|
|
|
}
|
2022-06-10 13:07:27 +02:00
|
|
|
this.value = val || '';
|
2022-06-02 16:12:38 +02:00
|
|
|
}
|
|
|
|
|
2022-03-18 21:25:54 +01:00
|
|
|
transformAttributes(attrs)
|
|
|
|
{
|
|
|
|
super.transformAttributes(attrs);
|
2022-06-17 19:41:29 +02:00
|
|
|
|
|
|
|
// Deal with initial value of multiple set as CSV
|
|
|
|
if(this.multiple && typeof this.value == "string")
|
|
|
|
{
|
|
|
|
this.value = this.value.length ? this.value.split(",") : [];
|
|
|
|
}
|
2022-03-18 21:25:54 +01:00
|
|
|
}
|
|
|
|
|
2022-07-04 23:08:00 +02:00
|
|
|
/**
|
|
|
|
* Load extra stuff from the template node.
|
|
|
|
* Overridden from parent to force value to be "good", since this is the earliest place we have both value and
|
|
|
|
* select options when loading from a template.
|
|
|
|
*
|
|
|
|
* @param {Element} _node
|
|
|
|
*/
|
|
|
|
loadFromXML(_node : Element)
|
|
|
|
{
|
|
|
|
super.loadFromXML(_node);
|
|
|
|
this.fix_bad_value();
|
|
|
|
}
|
|
|
|
|
2022-02-18 20:02:27 +01:00
|
|
|
/** @param {import('@lion/core').PropertyValues } changedProperties */
|
2022-06-23 23:55:42 +02:00
|
|
|
willUpdate(changedProperties : PropertyValues)
|
2022-01-13 23:28:52 +01:00
|
|
|
{
|
2022-06-23 23:55:42 +02:00
|
|
|
super.willUpdate(changedProperties);
|
2022-03-03 18:38:56 +01:00
|
|
|
|
2022-07-21 17:57:50 +02:00
|
|
|
if(changedProperties.has('select_options') || changedProperties.has("value") || changedProperties.has('emptyLabel'))
|
2022-01-04 23:38:10 +01:00
|
|
|
{
|
2022-06-23 23:55:42 +02:00
|
|
|
this.fix_bad_value();
|
2022-08-04 00:04:08 +02:00
|
|
|
}
|
|
|
|
if(changedProperties.has("select_options") && changedProperties.has("value"))
|
|
|
|
{
|
2022-02-18 20:02:27 +01:00
|
|
|
// Re-set value, the option for it may have just shown up
|
2022-08-04 00:04:08 +02:00
|
|
|
this.updateComplete.then(() => this.syncItemsFromValue())
|
2022-01-04 23:38:10 +01:00
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
|
|
|
|
2022-06-16 00:43:39 +02:00
|
|
|
/**
|
|
|
|
* Override this method from SlSelect to stick our own tags in there
|
|
|
|
*/
|
|
|
|
syncItemsFromValue()
|
|
|
|
{
|
|
|
|
if(typeof super.syncItemsFromValue === "function")
|
|
|
|
{
|
|
|
|
super.syncItemsFromValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only applies to multiple
|
|
|
|
if(typeof this.displayTags !== "object" || !this.multiple)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let overflow = null;
|
|
|
|
if(this.maxTagsVisible > 0 && this.displayTags.length > this.maxTagsVisible)
|
|
|
|
{
|
|
|
|
overflow = this.displayTags.pop();
|
|
|
|
}
|
2022-08-04 23:50:52 +02:00
|
|
|
const checkedItems = Object.values(this._menuItems).filter(item => this.value.includes(item.value));
|
2022-06-16 00:43:39 +02:00
|
|
|
this.displayTags = checkedItems.map(item => this._createTagNode(item));
|
|
|
|
|
|
|
|
// Re-slice & add overflow tag
|
|
|
|
if(overflow)
|
|
|
|
{
|
|
|
|
this.displayTags = this.displayTags.slice(0, this.maxTagsVisible);
|
|
|
|
this.displayTags.push(overflow);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-13 23:28:52 +01:00
|
|
|
_emptyLabelTemplate() : TemplateResult
|
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
if(!this.emptyLabel || this.multiple)
|
2022-01-04 23:38:10 +01:00
|
|
|
{
|
2022-01-13 23:28:52 +01:00
|
|
|
return html``;
|
2022-01-04 23:38:10 +01:00
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
return html`
|
2022-07-26 15:46:42 +02:00
|
|
|
<sl-menu-item value="">${this.emptyLabel}</sl-menu-item>`;
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
2022-01-04 23:38:10 +01:00
|
|
|
|
2022-06-16 00:43:39 +02:00
|
|
|
/**
|
|
|
|
* Tag used for rendering options
|
|
|
|
* Used for finding & filtering options, they're created by the mixed-in class
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
public get optionTag()
|
|
|
|
{
|
|
|
|
return "sl-menu-item";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-01-13 23:28:52 +01:00
|
|
|
_optionTemplate(option : SelectOption) : TemplateResult
|
|
|
|
{
|
2022-05-27 22:12:31 +02:00
|
|
|
let icon = option.icon ? html`
|
2022-06-10 22:11:57 +02:00
|
|
|
<et2-image slot="prefix" part="icon" style="width: var(--icon-width)"
|
|
|
|
src="${option.icon}"></et2-image>` : "";
|
2022-05-27 22:12:31 +02:00
|
|
|
|
2022-06-16 19:02:16 +02:00
|
|
|
// Tag used must match this.optionTag, but you can't use the variable directly.
|
|
|
|
// Pass option along so SearchMixin can grab it if needed
|
2022-01-13 23:28:52 +01:00
|
|
|
return html`
|
2022-07-26 15:06:07 +02:00
|
|
|
<sl-menu-item value="${option.value}" title="${!option.title || this.noLang ? option.title : this.egw().lang(option.title)}" class="${option.class}" .option=${option}>
|
2022-05-27 22:12:31 +02:00
|
|
|
${icon}
|
2022-07-26 15:06:07 +02:00
|
|
|
${this.noLang ? option.label : this.egw().lang(option.label)}
|
2022-05-27 22:12:31 +02:00
|
|
|
</sl-menu-item>`;
|
2022-01-04 23:38:10 +01:00
|
|
|
}
|
2022-06-16 00:43:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Tag used for rendering tags when multiple=true
|
|
|
|
* Used for creating, finding & filtering options.
|
|
|
|
* @see createTagNode()
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
public get tagTag()
|
|
|
|
{
|
|
|
|
return "et2-tag";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Customise how tags are rendered. This overrides what SlSelect
|
|
|
|
* does in syncItemsFromValue().
|
|
|
|
* This is a copy+paste from SlSelect.syncItemsFromValue().
|
|
|
|
*
|
|
|
|
* @param item
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _createTagNode(item)
|
|
|
|
{
|
|
|
|
const tag = <Et2Tag>document.createElement(this.tagTag);
|
|
|
|
tag.value = item.value;
|
2022-08-04 21:47:16 +02:00
|
|
|
tag.textContent = item.getTextLabel().trim();
|
2022-06-16 00:43:39 +02:00
|
|
|
tag.class = item.classList.value + " search_tag";
|
2022-06-24 22:12:04 +02:00
|
|
|
if(this.size)
|
|
|
|
{
|
|
|
|
tag.size = this.size;
|
|
|
|
}
|
2022-06-17 22:06:12 +02:00
|
|
|
if(this.readonly)
|
2022-06-16 00:43:39 +02:00
|
|
|
{
|
2022-06-17 22:06:12 +02:00
|
|
|
tag.removable = false;
|
|
|
|
tag.readonly = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
tag.addEventListener("dblclick", this._handleDoubleClick);
|
|
|
|
tag.addEventListener("click", this.handleTagInteraction);
|
|
|
|
tag.addEventListener("keydown", this.handleTagInteraction);
|
|
|
|
tag.addEventListener("sl-remove", (event) =>
|
2022-06-16 00:43:39 +02:00
|
|
|
{
|
2022-06-17 22:06:12 +02:00
|
|
|
event.stopPropagation();
|
|
|
|
if(!this.disabled)
|
|
|
|
{
|
|
|
|
item.checked = false;
|
|
|
|
this.syncValueFromItems();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-06-16 00:43:39 +02:00
|
|
|
let image = this._createImage(item);
|
|
|
|
if(image)
|
|
|
|
{
|
|
|
|
tag.prepend(image);
|
|
|
|
}
|
|
|
|
return tag;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _createImage(item)
|
|
|
|
{
|
|
|
|
let image = item.querySelector("et2-image");
|
|
|
|
if(image)
|
|
|
|
{
|
|
|
|
image = image.clone();
|
|
|
|
image.slot = "prefix";
|
|
|
|
image.class = "tag_image";
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2022-08-04 23:50:52 +02:00
|
|
|
public get _menuItems() : HTMLElement[]
|
|
|
|
{
|
|
|
|
return [...this.querySelectorAll<SlMenuItem>(this.optionTag)];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-29 19:49:24 +02:00
|
|
|
/**
|
|
|
|
* Override parent to always call validate(), as our simple implementation needs to validate on clear as well.
|
|
|
|
*
|
|
|
|
* @param {string | false} err
|
|
|
|
*/
|
|
|
|
set_validation_error(err : string | false)
|
|
|
|
{
|
|
|
|
super.set_validation_error(err);
|
|
|
|
if(err == false)
|
|
|
|
{
|
|
|
|
this.validate();
|
|
|
|
}
|
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
2022-01-04 23:38:10 +01:00
|
|
|
|
2022-06-10 18:25:31 +02:00
|
|
|
customElements.define("et2-select", Et2Select);
|
2022-06-29 19:49:24 +02:00
|
|
|
if(typeof customElements.get("lion-validation-feedback") === "undefined")
|
|
|
|
{
|
|
|
|
customElements.define("lion-validation-feedback", LionValidationFeedback);
|
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
/**
|
|
|
|
* Use a single StaticOptions, since it should have no state
|
|
|
|
* @type {StaticOptions}
|
|
|
|
*/
|
|
|
|
const so = new StaticOptions();
|
|
|
|
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectApp extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.app(this, {other: this.other || []});
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-app", Et2SelectApp);
|
|
|
|
|
2022-07-19 15:17:16 +02:00
|
|
|
export class Et2SelectTab extends Et2SelectApp
|
|
|
|
{
|
|
|
|
constructor()
|
|
|
|
{
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.allowFreeEntries = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
set value(new_value)
|
|
|
|
{
|
2022-07-19 19:47:03 +02:00
|
|
|
if(!new_value)
|
|
|
|
{
|
|
|
|
super.value = new_value;
|
|
|
|
return;
|
|
|
|
}
|
2022-07-19 15:17:16 +02:00
|
|
|
const values = Array.isArray(new_value) ? new_value : [new_value];
|
|
|
|
const options = this.select_options;
|
2022-07-19 19:47:03 +02:00
|
|
|
values.forEach(value =>
|
|
|
|
{
|
|
|
|
if(!options.filter(option => option.value == value).length)
|
2022-07-19 15:17:16 +02:00
|
|
|
{
|
|
|
|
const matches = value.match(/^([a-z0-9]+)\-/i);
|
|
|
|
let option : SelectOption = {value: value, label: value};
|
2022-07-19 19:47:03 +02:00
|
|
|
if(matches)
|
2022-07-19 15:17:16 +02:00
|
|
|
{
|
2022-07-19 19:47:03 +02:00
|
|
|
option = options.filter(option => option.value == matches[1])[0] || {
|
|
|
|
value: value,
|
|
|
|
label: this.egw().lang(matches[1])
|
|
|
|
};
|
2022-07-19 15:17:16 +02:00
|
|
|
option.value = value;
|
2022-07-19 19:47:03 +02:00
|
|
|
option.label += ' ' + this.egw().lang('Tab');
|
2022-07-19 15:17:16 +02:00
|
|
|
}
|
|
|
|
try {
|
|
|
|
const app = opener?.framework.getApplicationByName(value);
|
|
|
|
if (app && app.displayName)
|
|
|
|
{
|
|
|
|
option.label = app.displayName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
// ignore security exception, if opener is not accessible
|
|
|
|
}
|
|
|
|
this.select_options.concat(option);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
super.value = new_value;
|
|
|
|
}
|
|
|
|
|
|
|
|
get value()
|
|
|
|
{
|
|
|
|
return super.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-tab", Et2SelectTab);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectBitwise extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
|
|
|
set value(new_value)
|
|
|
|
{
|
|
|
|
let oldValue = this._value;
|
|
|
|
let expanded_value = [];
|
2022-03-16 21:36:23 +01:00
|
|
|
let options = this.select_options;
|
2022-01-17 23:12:46 +01:00
|
|
|
for(let index in options)
|
|
|
|
{
|
|
|
|
let right = parseInt(options[index].value);
|
|
|
|
if(!!(new_value & right))
|
|
|
|
{
|
|
|
|
expanded_value.push(right);
|
|
|
|
}
|
|
|
|
}
|
2022-05-27 22:12:31 +02:00
|
|
|
super.value = expanded_value;
|
2022-01-17 23:12:46 +01:00
|
|
|
|
|
|
|
this.requestUpdate("value", oldValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-bitwise", Et2SelectBitwise);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectBool extends Et2StaticSelectMixin(Et2Select)
|
2022-01-13 23:28:52 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-13 23:28:52 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
2022-01-17 23:12:46 +01:00
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.bool(this);
|
2022-03-16 21:36:23 +01:00
|
|
|
}
|
2022-01-13 23:28:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-bool", Et2SelectBool);
|
|
|
|
|
2022-01-14 17:29:14 +01:00
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectDay extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.day(this, {other: this.other || []});
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-day", Et2SelectDay);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectDayOfWeek extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.dow(this, {other: this.other || []});
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-dow", Et2SelectDayOfWeek);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectHour extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.hour(this, {other: this.other || []});
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-hour", Et2SelectHour);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectMonth extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.month(this);
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-month", Et2SelectMonth);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectNumber extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-06-03 18:18:18 +02:00
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
/**
|
|
|
|
* Step between numbers
|
|
|
|
*/
|
|
|
|
interval: {type: Number},
|
|
|
|
min: {type: Number},
|
|
|
|
max: {type: Number},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add one or more leading zeros
|
|
|
|
* Set to how many zeros you want (000)
|
|
|
|
*/
|
|
|
|
leading_zero: {type: String},
|
|
|
|
/**
|
|
|
|
* Appended after every number
|
|
|
|
*/
|
|
|
|
suffix: {type: String}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
2022-06-03 18:18:18 +02:00
|
|
|
this.min = 1;
|
|
|
|
this.max = 10;
|
|
|
|
this.interval = 1;
|
|
|
|
this.leading_zero = "";
|
|
|
|
this.suffix = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
2022-03-16 21:36:23 +01:00
|
|
|
|
2022-06-03 18:18:18 +02:00
|
|
|
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
|
|
|
|
{
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.number(this);
|
|
|
|
this.requestUpdate("select_options");
|
2022-06-03 18:18:18 +02:00
|
|
|
}
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-number", Et2SelectNumber);
|
|
|
|
|
2022-06-03 18:18:18 +02:00
|
|
|
export class Et2SelectPercent extends Et2SelectNumber
|
|
|
|
{
|
|
|
|
constructor()
|
|
|
|
{
|
|
|
|
super();
|
|
|
|
this.min = 0;
|
|
|
|
this.max = 100;
|
|
|
|
this.interval = 10;
|
|
|
|
this.suffix = "%%";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-percent", Et2SelectPercent);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectPriority extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.priority(this);
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-priority", Et2SelectPriority);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectState extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-06-18 15:06:50 +02:00
|
|
|
/**
|
|
|
|
* Two-letter ISO country code
|
|
|
|
*/
|
|
|
|
protected __country_code;
|
|
|
|
|
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
country_code: String,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
2022-06-18 15:06:50 +02:00
|
|
|
this.country_code = 'DE';
|
|
|
|
}
|
|
|
|
|
|
|
|
get country_code()
|
|
|
|
{
|
|
|
|
return this.__country_code;
|
|
|
|
}
|
|
|
|
|
|
|
|
set country_code(code : string)
|
|
|
|
{
|
|
|
|
this.__country_code = code;
|
2022-07-15 20:56:24 +02:00
|
|
|
this.static_options = so.state(this, {country_code: this.__country_code});
|
|
|
|
this.requestUpdate("select_options");
|
2022-06-18 15:06:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
set_country_code(code)
|
|
|
|
{
|
|
|
|
this.country_code = code;
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-state", Et2SelectState);
|
|
|
|
|
2022-07-15 20:56:24 +02:00
|
|
|
export class Et2SelectTimezone extends Et2StaticSelectMixin(Et2Select)
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
|
|
|
|
|
|
|
this.select_options = so.timezone(this, {other: this.other || []});
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
|
|
|
customElements.define("et2-select-timezone", Et2SelectTimezone);
|
|
|
|
|
2022-06-03 18:18:18 +02:00
|
|
|
export class Et2SelectYear extends Et2SelectNumber
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
constructor()
|
2022-01-17 23:12:46 +01:00
|
|
|
{
|
2022-03-16 21:36:23 +01:00
|
|
|
super();
|
2022-06-03 18:18:18 +02:00
|
|
|
this.min = -3;
|
|
|
|
this.max = 2;
|
|
|
|
}
|
2022-03-16 21:36:23 +01:00
|
|
|
|
2022-06-03 18:18:18 +02:00
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
|
|
|
|
|
|
|
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
|
|
|
|
{
|
|
|
|
this.select_options = so.year(this);
|
|
|
|
}
|
2022-01-17 23:12:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
2022-02-21 19:58:56 +01:00
|
|
|
customElements.define("et2-select-year", Et2SelectYear);
|