Feature/shoelace 2.4 upgrade (#135)

Update shoelace to 2.9.0
This commit is contained in:
Nathan Gray 2023-09-13 11:55:33 -06:00 committed by GitHub
parent 0f77eca5c4
commit e323cd1d79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 3852 additions and 1868 deletions

View File

@ -21,18 +21,20 @@ import {fetchAll, nm_action, nm_compare_field} from "../../api/js/etemplate/et2_
import "./CRM";
import {egw} from "../../api/js/jsapi/egw_global";
import {LitElement} from "@lion/core";
import {Et2SelectState} from "../../api/js/etemplate/Et2Select/Et2Select";
import {Et2SelectCountry} from "../../api/js/etemplate/Et2Select/Et2SelectCountry";
import {Et2SelectCountry} from "../../api/js/etemplate/Et2Select/Select/Et2SelectCountry";
import {Et2SelectState} from "../../api/js/etemplate/Et2Select/Select/Et2SelectState";
/**
* Object to call app.addressbook.openCRMview with
*/
export interface CrmParams {
contact_id: number|string;
crm_list?: "infolog"|"tracker"|"infolog-organisation"; // default: use preference
title?: string; // default: link-title of contact_id
icon?: string; // default: avatar for contact_id
index?: number;
export interface CrmParams
{
contact_id : number | string;
crm_list? : "infolog" | "tracker" | "infolog-organisation"; // default: use preference
title? : string; // default: link-title of contact_id
icon? : string; // default: avatar for contact_id
index? : number;
}
/**

View File

@ -1,4 +1,6 @@
import {Directive, directive, html, repeat} from "@lion/core";
import {html} from "lit";
import {Directive, directive} from "lit/directive.js";
import {repeat} from "lit/directives/repeat.js";
import {et2_activateLinks} from "./et2_core_common";
/**

View File

@ -9,7 +9,8 @@
*/
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {css, SlotMixin} from "@lion/core";
import {css} from "lit";
import {SlotMixin} from "@lion/core";
import {SlAvatar} from "@shoelace-style/shoelace";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {egw} from "../../jsapi/egw_global";

View File

@ -1,5 +1,6 @@
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {css, html, LitElement, repeat} from "@lion/core";
import {css, html, LitElement} from "lit";
import {repeat} from "lit/directives/repeat.js";
import shoelace from "../Styles/shoelace";
/**

View File

@ -10,7 +10,7 @@
import {Et2Avatar} from "./Et2Avatar";
import shoelace from "../Styles/shoelace";
import {css} from "@lion/core";
import {css} from "lit";
export class Et2LAvatar extends Et2Avatar
{

View File

@ -1,7 +1,7 @@
/**
* Cropper styles constant
*/
import {css} from "@lion/core";
import {css} from "lit";
/*!
* Cropper.js v1.5.12

View File

@ -9,7 +9,7 @@
*/
import {css, LitElement, PropertyValues} from "@lion/core";
import {css, LitElement, PropertyValues} from "lit";
import '../Et2Image/Et2Image';
import shoelace from "../Styles/shoelace";

View File

@ -13,7 +13,7 @@ import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import '../Et2Image/Et2Image';
import {SlButton} from "@shoelace-style/shoelace";
import {ButtonMixin} from "./ButtonMixin";
import {PropertyValues} from "@lion/core";
import {PropertyValues} from "lit";
export class Et2Button extends ButtonMixin(Et2InputWidget(SlButton))

View File

@ -14,7 +14,7 @@ import '../Et2Image/Et2Image';
import {SlIconButton} from "@shoelace-style/shoelace";
import {ButtonMixin} from "./ButtonMixin";
import shoelace from "../Styles/shoelace";
import {css} from "@lion/core";
import {css} from "lit";
export class Et2ButtonIcon extends ButtonMixin(Et2InputWidget(SlIconButton))

View File

@ -8,7 +8,7 @@
* @author Nathan Gray
*/
import {css, html, LitElement} from "@lion/core";
import {css, html, LitElement} from "lit";
import {ButtonMixin} from "./ButtonMixin";
/**

View File

@ -9,7 +9,7 @@
*/
import {css} from "@lion/core";
import {css} from "lit";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import '../Et2Image/Et2Image';
import {SlCheckbox} from "@shoelace-style/shoelace";

View File

@ -1,7 +1,8 @@
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {et2_checkbox} from "../et2_widget_checkbox";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {classMap, css, html, LitElement} from "@lion/core";
import {css, html, LitElement} from "lit";
import {classMap} from "lit/directives/class-map.js"
import shoelace from "../Styles/shoelace";
/**

View File

@ -9,7 +9,7 @@
*/
import {css, html, PropertyValues, render} from "@lion/core";
import {css, html, PropertyValues, render} from "lit";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {SlColorPicker} from "@shoelace-style/shoelace";
import shoelace from "../Styles/shoelace";

View File

@ -2,7 +2,7 @@
* Sharable date styles constant
*/
import {css} from "@lion/core";
import {css} from "lit";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {cssImage} from "../Et2Widget/Et2Widget";

View File

@ -9,7 +9,7 @@
*/
import {css, html} from "@lion/core";
import {css, html} from "lit";
import 'lit-flatpickr';
import {dateStyles} from "./DateStyles";
import type {Instance} from 'flatpickr/dist/types/instance';
@ -19,15 +19,11 @@ import flatpickr from "flatpickr";
import {egw} from "../../jsapi/egw_global";
import type {HTMLElementWithValue} from "@lion/form-core/types/FormControlMixinTypes";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {Et2ButtonIcon} from "../Et2Button/Et2ButtonIcon";
import {FormControlMixin} from "@lion/form-core";
import {LitFlatpickr} from "lit-flatpickr";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import shoelace from "../Styles/shoelace";
const textbox = new Et2Textbox();
const button = new Et2ButtonIcon();
// list of existing localizations from node_modules/flatpicker/dist/l10n directory:
const l10n = [
'ar', 'at', 'az', 'be', 'bg', 'bn', 'bs', 'cat', 'cs', 'cy', 'da', 'de', 'eo', 'es', 'et', 'fa', 'fi', 'fo',

View File

@ -9,7 +9,8 @@
*/
import {classMap, css, html, LitElement} from "@lion/core";
import {css, html, LitElement} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {sprintf} from "../../egw_action/egw_action_common";
import {dateStyles} from "./DateStyles";
@ -132,6 +133,7 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
}
.input-group__after {
display: contents;
margin-inline-start: var(--sl-input-spacing-medium);
}
@ -612,9 +614,9 @@ export class Et2DateDuration extends Et2InputWidget(FormControlMixin(LitElement)
<et2-select value="${this._display.unit || this.displayFormat[0]}">
${[...this.displayFormat].map((format : string) =>
html`
<sl-menu-item value=${format} ?checked=${this._display.unit === format}>
<sl-option value=${format}>
${this.time_formats[format]}
</sl-menu-item>`
</sl-option>`
)}
</et2-select>
`;

View File

@ -8,7 +8,7 @@
*/
import {css, html} from "@lion/core";
import {css, html} from "lit";
import {Et2DateDuration, formatOptions} from "./Et2DateDuration";
import {dateStyles} from "./DateStyles";

View File

@ -1,6 +1,8 @@
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {FormControlMixin} from "@lion/form-core";
import {classMap, css, html, ifDefined, LitElement, TemplateResult} from "@lion/core";
import {css, html, LitElement, TemplateResult} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {ifDefined} from "lit/directives/if-defined.js";
import shoelace from "../Styles/shoelace";
import {dateStyles} from "./DateStyles";
import {formatDate, parseDate} from "./Et2Date";

View File

@ -8,7 +8,7 @@
*/
import {html, LitElement} from "@lion/core";
import {html, LitElement} from "lit";
import {formatDate, parseDate} from "./Et2Date";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {Et2Widget} from "../Et2Widget/Et2Widget";

View File

@ -8,7 +8,7 @@
*/
import {html} from "@lion/core";
import {html} from "lit";
import {parseDate, parseDateTime} from "./Et2Date";
import {Et2DateReadonly} from "./Et2DateReadonly";

View File

@ -9,7 +9,7 @@
*/
import {css} from "@lion/core";
import {css} from "lit";
import {Et2Date, formatDate, formatDateTime} from "./Et2Date";
import type {Instance} from "flatpickr/dist/types/instance";
import {default as ShortcutButtonsPlugin} from "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr";

View File

@ -8,7 +8,7 @@
*/
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {css, html, LitElement, render} from "@lion/core";
import {css, html, LitElement, render} from "lit";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {activateLinks} from "../ActivateLinksDirective";
import {et2_csvSplit} from "../et2_core_common";

View File

@ -12,7 +12,12 @@
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_button} from "../et2_widget_button";
import {et2_widget} from "../et2_core_widget";
import {classMap, css, html, ifDefined, LitElement, render, repeat, SlotMixin, styleMap} from "@lion/core";
import {css, html, LitElement, render} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {ifDefined} from "lit/directives/if-defined.js";
import {repeat} from "lit/directives/repeat.js";
import {styleMap} from "lit/directives/style-map.js";
import {SlotMixin} from "@lion/core";
import {et2_template} from "../et2_widget_template";
import {etemplate2} from "../etemplate2";
import {egw, IegwAppLocal} from "../../jsapi/egw_global";

View File

@ -9,11 +9,11 @@
*/
import {Et2Button} from "../Et2Button/Et2Button";
import {SlButtonGroup, SlDropdown} from "@shoelace-style/shoelace";
import {css, html, TemplateResult} from "@lion/core";
import {css, html, LitElement, TemplateResult} from "lit";
import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
import {SelectOption} from "../Et2Select/FindSelectOptions";
import shoelace from "../Styles/shoelace";
/**
* A split button - a button with a dropdown list
@ -28,13 +28,14 @@ import {SelectOption} from "../Et2Select/FindSelectOptions";
* as for a select box, but the title can also be full HTML if needed.
*
*/
export class Et2DropdownButton extends Et2WidgetWithSelectMixin(Et2Button)
export class Et2DropdownButton extends Et2WidgetWithSelectMixin(LitElement)
{
static get styles()
{
return [
...super.styles,
shoelace,
css`
:host {
/* Avoid unwanted style overlap from button */
@ -98,6 +99,7 @@ export class Et2DropdownButton extends Et2WidgetWithSelectMixin(Et2Button)
// We have our own render, so we can handle it internally
}
render() : TemplateResult
{
if(this.readonly)
@ -129,10 +131,10 @@ export class Et2DropdownButton extends Et2WidgetWithSelectMixin(Et2Button)
<et2-image slot="prefix" src=${option.icon} icon></et2-image>` : '';
return html`
<sl-menu-item value="${option.value}">
<sl-option value="${option.value}">
${icon}
${this.noLang ? option.label : this.egw().lang(option.label)}
</sl-menu-item>`;
</sl-option>`;
}
protected _handleSelect(ev)

View File

@ -10,7 +10,7 @@
*/
import {Et2DropdownButton} from "../Et2DropdownButton/Et2DropdownButton";
import {css, html, PropertyValues, TemplateResult} from "@lion/core";
import {css, html, PropertyValues, TemplateResult} from "lit";
import {SelectOption} from "../Et2Select/FindSelectOptions";
import {et2_INextmatchHeader, et2_nextmatch} from "../et2_extension_nextmatch";
import {Et2Image} from "../Et2Image/Et2Image";
@ -76,24 +76,24 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
min-width: 15em;
}
sl-menu-item:hover et2-image[src="trash"] {
sl-option:hover et2-image[src="trash"] {
display: initial;
}
/* Add star icons - radio button is already in prefix */
sl-menu-item::part(base) {
sl-option::part(base) {
background-image: ${cssImage("fav_filter")};
background-repeat: no-repeat;
background-size: 16px 16px;
background-position: 5px center;
}
sl-menu-item[checked]::part(base) {
sl-option[checked]::part(base) {
background-image: ${cssImage("favorites")};
}
sl-menu-item:last-child::part(base) {
sl-option:last-child::part(base) {
background-image: none;
}
`,
@ -185,11 +185,11 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
statustext="${this.egw().lang("Delete")}"></et2-image>`;
return html`
<sl-menu-item value="${option.value}" ?checked="${option.value == this._preferred}">
<sl-option value="${option.value}" ?checked="${option.value == this._preferred}">
${option.value !== Et2Favorites.ADD_VALUE ? radio : ""}
${icon}
${option.label}
</sl-menu-item>`;
</sl-option>`;
}

View File

@ -9,7 +9,8 @@
*/
import {css, html, LitElement, SlotMixin} from "@lion/core";
import {css, html, LitElement} from "lit";
import {SlotMixin} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
export class Et2Iframe extends Et2Widget(SlotMixin(LitElement))

View File

@ -8,8 +8,8 @@
* @author Nathan Gray
*/
import {css, html, LitElement, render, SlotMixin} from "@lion/core";
import {css, html, LitElement, render} from "lit";
import {SlotMixin} from "@lion/core";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../et2_core_interfaces";

View File

@ -1,10 +1,11 @@
import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "../et2_core_interfaces";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {css, dedupeMixin, LitElement, PropertyValues} from "@lion/core";
import {css, LitElement, PropertyValues} from "lit";
import {Required} from "../Validators/Required";
import {ManualMessage} from "../Validators/ManualMessage";
import {LionValidationFeedback, Validator} from "@lion/form-core";
import {et2_csvSplit} from "../et2_core_common";
import {dedupeMixin} from "@lion/core";
/**
* This mixin will allow any LitElement to become an Et2InputWidget

View File

@ -11,7 +11,7 @@
import {ExposeMixin, ExposeValue} from "../Expose/ExposeMixin";
import {css, html, LitElement, TemplateResult} from "@lion/core";
import {css, html, LitElement, TemplateResult} from "lit";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../et2_core_interfaces";

View File

@ -1,6 +1,7 @@
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {css, html, LitElement, PropertyValues} from "lit";
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import {css, html, LitElement, PropertyValues, SlotMixin} from "@lion/core";
import {SlotMixin} from "@lion/core";
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
import {LinkInfo} from "./Et2Link";
import {Et2Button} from "../Et2Button/Et2Button";

View File

@ -1,9 +1,9 @@
import {cleanSelectOptions, SelectOption} from "../Et2Select/FindSelectOptions";
import {css, html, SlotMixin, TemplateResult} from "@lion/core";
import {css, html, TemplateResult} from "lit";
import {Et2Select} from "../Et2Select/Et2Select";
export class Et2LinkAppSelect extends SlotMixin(Et2Select)
export class Et2LinkAppSelect extends Et2Select
{
static get styles()
{
@ -49,22 +49,11 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
}
};
get slots()
{
return {
...super.slots,
"": () =>
{
/*
icon.style.width = "var(--icon-width)";
icon.style.height = "var(--icon-width)";
const icon = document.createElement("et2-image");
icon.setAttribute("slot", "prefix");
icon.setAttribute("src", "api/navbar");
icon.style.width = "var(--icon-width)";
icon.style.height = "var(--icon-width)";
return icon;
}
}
}
*/
protected __applicationList : string[];
protected __onlyApp : string;
@ -104,9 +93,6 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
{
super.connectedCallback();
// Set icon
this.querySelector(":scope > [slot='prefix']").setAttribute("src", this.egw().link_get_registry(this.value, 'icon') ?? this.value + "/navbar");
if(!this.value)
{
// use preference
@ -118,7 +104,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
this.value = this.egw().preference('link_app', appname || this.egw().app_name());
}
// Register to
this.addEventListener("change", this._handleChange);
this.addEventListener("sl-change", this._handleChange);
if(this.__onlyApp)
{
@ -129,7 +115,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
disconnectedCallback()
{
super.disconnectedCallback();
this.removeEventListener("change", this._handleChange);
this.removeEventListener("sl-change", this._handleChange);
}
/**
@ -173,10 +159,9 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
super.value = new_value;
}
_handleChange(e)
handleValueChange(e)
{
// Set icon
this.querySelector(":scope > [slot='prefix']").setAttribute("src", this.egw().link_get_registry(this.value, 'icon'));
super.handleValueChange(e);
// update preference
let appname = "";
@ -198,13 +183,21 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
// Limit to one app
if(this.onlyApp)
{
select_options.push({value: this.onlyApp, label: this.egw().lang(this.onlyApp)});
select_options.push({
value: this.onlyApp,
label: this.egw().lang(this.onlyApp),
icon: this.egw().link_get_registry(this.onlyApp, 'icon') ?? this.onlyApp + "/navbar"
});
}
else if(this.applicationList.length > 0)
{
select_options = this.applicationList.map((app) =>
{
return {value: app, label: this.egw().lang(app)};
return {
value: app,
label: this.egw().lang(app),
icon: this.egw().link_get_registry(app, 'icon') ?? app + "/navbar"
};
});
}
else
@ -215,29 +208,27 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
{
delete select_options['addressbook-email'];
}
select_options = cleanSelectOptions(select_options);
select_options.map((option) =>
{
option.icon = this.egw().link_get_registry(option.value, 'icon') ?? option.value + "/navbar"
});
}
if (!this.value)
{
this.value = <string>this.egw().preference('link_app', this.egw().app_name());
}
this.select_options = cleanSelectOptions(select_options);
this.select_options = select_options;
}
_optionTemplate(option : SelectOption) : TemplateResult
{
return html`
<sl-menu-item value="${option.value}" title="${option.title}">
<sl-option value="${option.value}" title="${option.title}">
${this.appIcons ? "" : option.label}
${this._iconTemplate(option.value)}
</sl-menu-item>`;
}
_iconTemplate(appname)
{
let url = appname ? this.egw().link_get_registry(appname, 'icon') : "";
return html`
<et2-image style="width: var(--icon-width)" slot="prefix" src="${url}"></et2-image>`;
${this._iconTemplate(option)}
</sl-option>`;
}
}

View File

@ -6,8 +6,8 @@
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html, LitElement, PropertyValues, SlotMixin} from "@lion/core";
import {css, html, LitElement, PropertyValues} from "lit";
import {SlotMixin} from "@lion/core";
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {FormControlMixin} from "@lion/form-core";

View File

@ -10,8 +10,9 @@
*/
import {css, html, repeat, TemplateResult} from "@lion/core";
import {Et2Link, LinkInfo} from "./Et2Link";
import {css, html, TemplateResult} from "lit";
import {repeat} from "lit/directives/repeat.js";
import {LinkInfo} from "./Et2Link";
import {egw} from "../../jsapi/egw_global";
import {Et2LinkString} from "./Et2LinkString";
import {egwMenu} from "../../egw_action/egw_menu";

View File

@ -7,7 +7,7 @@
* @author Nathan Gray
*/
import {css} from "@lion/core";
import {css} from "lit";
import {Et2Select} from "../Et2Select/Et2Select";
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
import {Et2Link} from "./Et2Link";
@ -86,9 +86,9 @@ export class Et2LinkSearch extends Et2Select
super.updated(changedProperties);
// Set a value we don't have as an option? That's OK, we'll just add it
if(changedProperties.has("value") && this.value && (
this.menuItems && this.menuItems.length == 0 ||
this.menuItems?.filter && this.menuItems.filter(item => this.value.includes(item.value)).length == 0
if(changedProperties.has("value") && this.value && this.value.length > 0 && (
this.getAllOptions().length == 0 ||
this.getAllOptions().filter && this.getAllOptions().filter(item => this.getValueAsArray().includes(item.value)).length == 0
))
{
this._missingOption(this.value)
@ -120,19 +120,16 @@ export class Et2LinkSearch extends Et2Select
option.label = title || Et2Link.MISSING_TITLE;
option.class = "";
// It's probably already been rendered, find the item
let item = this.menuItems.find(i => i.value === option.value);
let item = this.getAllOptions().find(i => i.value === option.value);
if(item)
{
item.textContent = title;
item.classList.remove("loading");
this.syncItemsFromValue();
}
else
{
// Not already rendered, update the select option
this.requestUpdate("select_options");
// update the displayed text
this.updateComplete.then(() => this.syncItemsFromValue());
}
});
}

View File

@ -10,7 +10,8 @@
*/
import {css, html, LitElement, PropertyValues, render, TemplateResult, until} from "@lion/core";
import {css, html, LitElement, PropertyValues, render, TemplateResult} from "lit";
import {until} from "lit/directives/until.js";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {Et2Link, LinkInfo} from "./Et2Link";
import {et2_IDetachedDOM} from "../et2_core_interfaces";

View File

@ -12,7 +12,8 @@
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import {css, html, LitElement, ScopedElementsMixin} from "@lion/core";
import {css, html, LitElement} from "lit";
import {ScopedElementsMixin} from "@lion/core";
import {et2_createWidget, et2_widget} from "../et2_core_widget";
import {et2_file} from "../et2_widget_file";
import {Et2Button} from "../Et2Button/Et2Button";

View File

@ -1,7 +1,9 @@
/**
* Column selector for nextmatch
*/
import {classMap, css, html, LitElement, repeat, TemplateResult} from "@lion/core";
import {css, html, LitElement, TemplateResult} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {repeat} from "lit/directives/repeat.js";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {et2_nextmatch_customfields} from "../et2_extension_nextmatch";
import shoelace from "../Styles/shoelace";
@ -85,7 +87,7 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
{
this.sort = Sortable.create(this.shadowRoot.querySelector('sl-menu'), {
ghostClass: 'ui-fav-sortable-placeholder',
draggable: 'sl-menu-item.column',
draggable: 'sl-option.column',
dataIdAttr: 'value',
direction: 'vertical',
delay: 25
@ -141,10 +143,11 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
return html``;
}
return html`
<sl-menu-item
<sl-option
value="${column.id}"
?checked=${alwaysOn || column.visibility == et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE}
?disabled=${alwaysOn}
.selected="${this.value.some(v => v == column.id)}"
title="${column.title}"
class="${classMap({
select_row: true,
@ -154,7 +157,7 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
${column.caption}
<!-- Custom fields get listed separately -->
${isCustom ? this.customFieldsTemplate(column) : ''}
</sl-menu-item>`;
</sl-option>`;
}
/**
@ -198,8 +201,8 @@ export class Et2ColumnSelection extends Et2InputWidget(LitElement)
handleSelectAll(event)
{
let checked = (<SlMenuItem>this.shadowRoot.querySelector("sl-menu-item")).checked || false;
this.shadowRoot.querySelectorAll('sl-menu-item').forEach((item) => {item.checked = !checked});
let checked = (<SlMenuItem>this.shadowRoot.querySelector("sl-option")).checked || false;
this.shadowRoot.querySelectorAll('sl-option').forEach((item) => {item.checked = !checked});
}
set columns(new_columns)

View File

@ -1,4 +1,4 @@
import {Et2SelectAccount} from "../../Et2Select/Et2SelectAccount";
import {Et2SelectAccount} from "../../Et2Select/Select/Et2SelectAccount";
import {et2_INextmatchHeader} from "../../et2_extension_nextmatch";
import {FilterMixin} from "./FilterMixin";

View File

@ -2,7 +2,7 @@ import {loadWebComponent} from "../../Et2Widget/Et2Widget";
import {Et2Select} from "../../Et2Select/Et2Select";
import {Et2InputWidget, Et2InputWidgetInterface} from "../../Et2InputWidget/Et2InputWidget";
import {FilterMixin} from "./FilterMixin";
import {html, LitElement} from "@lion/core";
import {html, LitElement} from "lit";
/**
* Filter by some other type of widget

View File

@ -1,6 +1,6 @@
import {egw} from "../../../jsapi/egw_global";
import {et2_INextmatchHeader, et2_nextmatch} from "../../et2_extension_nextmatch";
import {LitElement} from "@lion/core";
import {LitElement} from "lit";
// Export the Interface for TypeScript
type Constructor<T = LitElement> = new (...args : any[]) => T;

View File

@ -15,8 +15,9 @@ import {SlCard} from "@shoelace-style/shoelace";
import interact from "@interactjs/interactjs";
import type {InteractEvent} from "@interactjs/core/InteractEvent";
import {egw} from "../../jsapi/egw_global";
import {classMap, css, html, TemplateResult} from "@lion/core";
import {HasSlotController} from "@shoelace-style/shoelace/dist/internal/slot";
import {css, html, TemplateResult} from "lit";
import {classMap} from "lit/directives/class-map.js";
import type {HasSlotController} from "../../../../node_modules/@shoelace-style/shoelace/dist/internal/slot";
import shoelace from "../Styles/shoelace";
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
import {et2_IResizeable} from "../et2_core_interfaces";

View File

@ -2,7 +2,7 @@ import {SlMenu} from "@shoelace-style/shoelace";
import {Et2WidgetWithSelectMixin} from "./Et2WidgetWithSelectMixin";
import {RowLimitedMixin} from "../Layout/RowLimitedMixin";
import shoelace from "../Styles/shoelace";
import {css, html, TemplateResult} from "@lion/core";
import {css, html, TemplateResult} from "lit";
import {SelectOption} from "./FindSelectOptions";
/**
@ -40,7 +40,8 @@ export class Et2Listbox extends RowLimitedMixin(Et2WidgetWithSelectMixin(SlMenu)
overflow-x: clip;
}
/* Ellipsis when too small */
sl-menu-item.menu-item__label {
sl-option.option__label {
display: block;
text-overflow: ellipsis;
/* This is usually not used due to flex, but is the basis for ellipsis calculation */
@ -153,15 +154,15 @@ export class Et2Listbox extends RowLimitedMixin(Et2WidgetWithSelectMixin(SlMenu)
// Tag used must match this.optionTag, but you can't use the variable directly.
// Pass option along so SearchMixin can grab it if needed
return html`
<sl-menu-item
<sl-option
value="${option.value}"
title="${!option.title || this.noLang ? option.title : this.egw().lang(option.title)}"
class="${option.class}" .option=${option}
?checked=${checked}
.selected=${checked}
>
${icon}
${this.noLang ? option.label : this.egw().lang(option.label)}
</sl-menu-item>`;
</sl-option>`;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,8 @@
*/
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
import {html, LitElement, PropertyValues, render, TemplateResult} from "@lion/core";
import {html, LitElement, PropertyValues, render, TemplateResult} from "lit";
import {property} from "lit/decorators/property.js";
import {et2_readAttrWithDefault} from "../et2_core_xml";
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
import {SearchMixinInterface} from "./SearchMixin";
@ -17,7 +18,7 @@ import {SearchMixinInterface} from "./SearchMixin";
* 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.
*
* As with most other widgets that extend Lion components, do not override render().
* As with most other widgets that extend Shoelace components, do not override render() without good reason.
* 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.
@ -46,15 +47,6 @@ import {SearchMixinInterface} from "./SearchMixin";
* 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:
* 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.
*
*/
// Export the Interface for TypeScript
type Constructor<T = {}> = new (...args : any[]) => T;
@ -63,29 +55,46 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
{
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 ''
*/
emptyLabel: String,
/**
* 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.select_options = SelectOption[]
*/
select_options: {type: Object, noAccessor: true},
/**
* Limit size
*/
rows: {type: Number, noAccessor: true, reflect: true}
/**
* The current value of the select, submitted as a name/value pair with form data. When `multiple` is enabled, the
* value attribute will be a space-delimited list of values based on the options selected, and the value property will
* be an array.
*
@property({
noAccessor: true,
converter: {
fromAttribute: (value : string) => value.split(',')
}
}
})
value : string | string[] = "";
*/
/**
* Textual label for first row, eg: 'All' or 'None'. It's value will be ''
*/
@property({type: String})
emptyLabel : String = "";
/**
* Limit size
*/
@property({type: Number, noAccessor: true, reflect: true})
/**
* Internal list of possible select options
*
* This is where we keep options sent from the server. This is not always the complete list, as extending
* classes may have their own options to add in. For example, static options are kept separate, as are search
* results. The select_options getter should give the complete list.
*/
private __select_options : SelectOption[] = [];
/**
* When we create the select option elements, it takes a while.
* If we don't wait for them, it causes issues in SlSelect
*/
protected _optionRenderPromise : Promise<void> = Promise.resolve();
/**
* Options found in the XML when reading the template
@ -101,6 +110,13 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
this.__select_options = <SelectOption[]>[];
}
async getUpdateComplete() : Promise<boolean>
{
const result = await super.getUpdateComplete();
await this._optionRenderPromise;
return result;
}
/** @param {import('@lion/core').PropertyValues } changedProperties */
updated(changedProperties : PropertyValues)
{
@ -116,26 +132,48 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
}
}
}
willUpdate(changedProperties : PropertyValues<this>)
{
// Add in actual option tags to the DOM based on the new select_options
if(changedProperties.has('select_options') || changedProperties.has("emptyLabel"))
{
// Add in options as children to the target node
this._renderOptions();
const optionPromise = this._renderOptions();
// This is needed to display initial load value in some cases, like infolog nm header filters
if(this.handleMenuSlotChange && !this.hasUpdated)
if(typeof this.selectionChanged !== "undefined")
{
this.handleMenuSlotChange();
optionPromise.then(async() =>
{
await this.updateComplete;
this.selectionChanged();
});
}
}
}
public getValueAsArray()
{
if(Array.isArray(this.value))
{
return this.value;
}
if(this.value == "null" || typeof this.value == "undefined" || !this.emptyLabel && this.value == "")
{
return [];
}
return [this.value];
}
/**
* Render select_options as child DOM Nodes
* @protected
*/
protected _renderOptions()
{
return Promise.resolve();
// Add in options as children to the target node
if(!this._optionTargetNode)
{
@ -156,7 +194,7 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
.map(this._groupTemplate.bind(this))}`;
render(options, temp_target);
return Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
this._optionRenderPromise = Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
.then(() =>
{
this._optionTargetNode.replaceChildren(
@ -168,23 +206,7 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
this.handleMenuSlotChange();
}
});
}
/**
* 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);
return this._optionRenderPromise;
}
/**
@ -212,6 +234,13 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
this.select_options = <SelectOption[]>new_options;
}
/**
* 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.select_options = SelectOption[]
*/
@property({type: Object})
get select_options() : SelectOption[]
{
return this.__select_options;
@ -262,7 +291,7 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
* @param {SelectOption} option
* @returns {TemplateResult}
*/
_optionTemplate(option : SelectOption) : TemplateResult
protected _optionTemplate(option : SelectOption) : TemplateResult
{
return html`
<span>Override _optionTemplate(). ${option.value} => ${option.label}</span>`;
@ -276,7 +305,7 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
}
return html`
<sl-menu-label>${this.noLang ? option.label : this.egw().lang(option.label)}</sl-menu-label>
<small>${this.noLang ? option.label : this.egw().lang(option.label)}</small>
${option.value.map(this._optionTemplate.bind(this))}
<sl-divider></sl-divider>
`;

View File

@ -18,6 +18,9 @@ export interface SelectOption
// Show the option, but it is not selectable.
// If multiple=true and the option is in the value, it is not removable.
disabled? : boolean;
// If a search is in progress, does this option match.
// Automatically changed.
isMatch? : boolean;
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -7,14 +7,13 @@
* @author Nathan Gray
*/
import {css, html, LitElement, render, SlotMixin} from "@lion/core";
import {css, CSSResultGroup, html, LitElement, nothing, render, TemplateResult} from "lit";
import {cleanSelectOptions, SelectOption} from "./FindSelectOptions";
import {Validator} from "@lion/form-core";
import {Et2Tag} from "./Tag/Et2Tag";
import {SlMenuItem} from "@shoelace-style/shoelace";
import {waitForEvent} from "@shoelace-style/shoelace/dist/internal/event";
import {StaticOptions} from "./StaticOptions";
import {dedupeMixin} from "@open-wc/dedupe-mixin";
// Otherwise import gets stripped
let keep_import : Et2Tag;
@ -66,6 +65,13 @@ export declare class SearchMixinInterface
* Check a [local] item to see if it matches
*/
searchMatch(search : string, options : object, item : LitElement) : boolean
/**
* Additional customisation location, where we stick the search elements
*
* @type {TemplateResult}
*/
_extraTemplate : TemplateResult
}
/**
@ -74,9 +80,9 @@ export declare class SearchMixinInterface
*
* Currently I assume we're extending an Et2Select, so changes may need to be made for better abstraction
*/
export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass : T) =>
export const Et2WithSearchMixin = dedupeMixin(<T extends Constructor<LitElement>>(superclass : T) =>
{
class Et2WidgetWithSearch extends SlotMixin(superclass)
class Et2WidgetWithSearch extends superclass
{
static get properties()
{
@ -105,54 +111,18 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
}
static get styles()
static get styles() : CSSResultGroup
{
return [
// @ts-ignore
...(super.styles ? (Symbol.iterator in Object(super.styles) ? super.styles : [super.styles]) : []),
css`
/* Move the widget border
.form-control-input {
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
border-radius: var(--sl-input-border-radius-medium);
}
.form-control-input:hover {
background-color: var(--sl-input-background-color-hover);
border-color: var(--sl-input-border-color-hover);
color: var(--sl-input-color-hover);
}
.select--standard .select__control {
border-style: none;
}
/* Move focus highlight */
.form-control-input:focus-within {
box-shadow: var(--sl-focus-ring);
}
.select--standard.select--focused:not(.select--disabled) .select__control {
box-shadow: initial;
}
/* Show / hide SlSelect icons - dropdown arrow, etc but not loading spinner */
:host([allowFreeEntries]) ::slotted(sl-icon[slot="suffix"]) {
display: none;
}
/* Make search textbox take full width */
::slotted(.search_input), ::slotted(.search_input) input, .search_input, .search_input input {
width: 100%;
}
.search_input input {
flex: 1 1 auto;
width: 100%;
}
/* Full width search textbox covers loading spinner, lift it up */
::slotted(sl-spinner) {
z-index: 2;
}
/* Don't show the current value while searching for single, we want the space
This lets the current value shrink to nothing so the input can expand
*/
.select__label {
flex: 1 15 auto;
}
/* Show edit textbox only when editing */
.search_input #edit {
display: none;
@ -163,36 +133,55 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
.search_input.editing #edit {
display: initial;
}
:host([search]:not([multiple])) .select--open .select__prefix {
:host([search]) sl-select[open]::part(prefix), :host([allowfreeentries]) sl-select[open]::part(prefix) {
order: 9;
flex: 2 1 auto;
flex-wrap: wrap;
width: 100%;
}
:host([search]:not([multiple])) .select--open .select__label {
margin: 0px;
}
:host([allowfreeentries]:not([multiple])) .select--standard.select--open:not(.select--disabled) .select__control .select__prefix {
flex: 1 1 auto;
}
:host([allowfreeentries]:not([multiple])) .select--standard.select--open:not(.select--disabled) .select__control .select__label {
:host([search]) sl-select[open]::part(display-input), :host([allowfreeentries]) sl-select[open]::part(display-input) {
display: none;
}
:host([search][multiple]) sl-select[open]::part(expand-icon) {
display: none;
}
:host([multiple]) sl-select[open]::part(tags) {
flex-basis: 100%;
}
:host([multiple]) sl-select[open]::part(combobox) {
flex-flow: wrap;
}
/* Search textbox general styling, starts hidden */
.select__prefix ::slotted(.search_input), .search_input {
.search_input {
display: none;
/* See also etemplate2.css, searchbox border turned off in there */
border: none;
flex: 1 1 auto;
order: 2;
margin-left: 0px;
width: 100%;
height: var(--sl-input-height-medium);
position: absolute;
width: 100%;
background-color: white;
z-index: var(--sl-z-index-dropdown);
}
:host([search]) et2-textbox::part(base) {
border: none;
box-shadow: none;
}
/* Search UI active - show textbox & stuff */
::slotted(.search_input.active), .search_input.active,
.search_input.active,
.search_input.editing {
display: flex;
}
@ -204,7 +193,8 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
/* Hide options that do not match current search text */
::slotted(.no-match) {
.no-match {
display: none;
}
/* Different cursor for editable tags */
@ -221,10 +211,6 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
:host([readonly]) .form-control-input:focus-within {
box-shadow: none;
}
/* no menu */
:host([readonly]) sl-menu {
display: none;
}
/* normal cursor */
:host([readonly]) .select__control {
cursor: initial;
@ -264,6 +250,11 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// Hold the original option data from earlier search results, since we discard on subsequent search
private _selected_remote = <SelectOption[]>[];
// Hold current search results, selected or otherwise
private _remote_options = <SelectOption[]>[];
private _total_result_count = 0;
/**
* These characters will end a free tag
* @type {string[]}
@ -283,7 +274,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// Hiding the selected options from the dropdown means we can't un-select the tags
// hidden by the max limit. Prefer no limit.
this.maxTagsVisible = -1;
this.maxOptionsVisible = -1;
this.validators = [];
/**
@ -297,16 +288,19 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
this.defaultValidators = [];
this.handleMenuSelect = this.handleMenuSelect.bind(this);
this.handleOptionClick = this.handleOptionClick.bind(this);
this._handleChange = this._handleChange.bind(this);
this.handleTagEdit = this.handleTagEdit.bind(this);
this._handleAfterShow = this._handleAfterShow.bind(this);
this._handleMenuHide = this._handleMenuHide.bind(this);
this._handleSearchBlur = this._handleSearchBlur.bind(this);
this._handleClear = this._handleClear.bind(this);
this._handleDoubleClick = this._handleDoubleClick.bind(this);
this._handleSearchAbort = this._handleSearchAbort.bind(this);
this._handleSearchClear = this._handleSearchClear.bind(this);
this._handleSearchChange = this._handleSearchChange.bind(this);
this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this);
this._handleSearchMouseDown = this._handleSearchMouseDown.bind(this);
this._handleEditKeyDown = this._handleEditKeyDown.bind(this);
this._handlePaste = this._handlePaste.bind(this);
}
@ -324,7 +318,6 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
return;
}
this._addNodes();
this._bindListeners();
}
@ -363,7 +356,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
else if(this.allowFreeEntries && this.multiple)
{
this.value.forEach((e) =>
this.getValueAsArray().forEach((e) =>
{
if(!this.select_options.find(o => o.value == e))
{
@ -402,7 +395,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
// Normally this should be handled in render(), but we have to add our nodes in
this._addNodes();
//this._addNodes();
}
// Update any tags if edit mode changes
if(changedProperties.has("editModeEnabled") || changedProperties.has("readonly"))
@ -416,54 +409,33 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
}
/**
* Add the nodes we need to search - adjust parent shadowDOM
*
* @protected
*/
protected _addNodes()
protected _extraTemplate()
{
if(this._activeControls)
if(!this.searchEnabled && !this.editModeEnabled && !this.allowFreeEntries || this.readonly)
{
// Already there
return;
return nothing;
}
const div = document.createElement("div");
div.classList.add("search_input");
render(this._searchInputTemplate(), div);
if(!super.multiple)
{
div.slot = "prefix";
this.appendChild(div);
return;
}
super.updateComplete.then(() =>
{
let control = this.shadowRoot.querySelector(".form-control-input");
control.append(div);
});
return html`
${this._searchInputTemplate()}
${this._moreResultsTemplate()}
`;
}
/**
* Customise how tags are rendered.
* Override to add edit
*
* @param item
* @protected
*/
protected _createTagNode(item)
protected _moreResultsTemplate()
{
let tag = <Et2Tag>document.createElement(this.tagTag);
tag.editable = this.editModeEnabled && !this.readonly;
if(this._total_result_count == 0 || this._total_result_count - this._remote_options.length == 0)
{
return nothing;
}
const more = this.egw().lang("%1 more...", this._total_result_count - this._remote_options.length);
return tag;
return html`<span class="more">${more}</span>`;
}
protected _searchInputTemplate()
{
let edit = null;
let edit = nothing;
if(this.editModeEnabled)
{
edit = html`<input id="edit" type="text" part="input" autocomplete="off" style="width:100%"
@ -472,16 +444,20 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
@blur=${this.stopEdit.bind(this)}
/>`;
}
// I can't figure out how to get this full width via CSS
return html`
<et2-textbox id="search" type="text" part="input" clearable
<div class="search_input" slot="prefix">
<et2-textbox id="search" type="text" part="input"
exportparts="base:search__base"
clearable
autocomplete="off"
placeholder="${this.egw().lang("search")}"
style="width:100%"
style="flex: 1 1 auto;"
@keydown=${this._handleSearchKeyDown}
@blur=${this._handleSearchBlur}
@sl-clear=${this._handleSearchClear}
></et2-textbox>
${edit}
</div>
`;
}
@ -517,6 +493,10 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this.querySelector(".search_input");
}
protected get optionTag()
{
return 'sl-option';
}
/**
* Only local options, excludes server options
@ -525,7 +505,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
protected get localItems() : NodeList
{
return this.querySelectorAll(this.optionTag + ":not(.remote)");
return this.select.querySelectorAll(this.optionTag + ":not(.remote)");
}
/**
@ -535,7 +515,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
protected get remoteItems() : NodeList
{
return this.querySelectorAll(this.optionTag + ".remote");
return this.select?.querySelectorAll(this.optionTag + ".remote") ?? [];
}
/**
@ -545,7 +525,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
protected get freeEntries() : NodeList
{
return this.querySelectorAll(this.optionTag + ".freeEntry");
return this.select?.querySelectorAll(this.optionTag + ".freeEntry") ?? [];
}
get select_options() : SelectOption[]
@ -558,6 +538,9 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// Any kept remote options
options = options.concat(this._selected_remote ?? []);
// Current search results
options = options.concat(this._remote_options ?? []);
if(this.allowFreeEntries)
{
this.freeEntries.forEach((item : SlMenuItem) =>
@ -600,11 +583,11 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
return;
}
// If widget is currently open, we may need to re-calculate search / dropdown positioning
if(this.isOpen)
{
this.handleMenuShow();
this._handleMenuShow();
}
}
@ -641,7 +624,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
const valueArray = Array.isArray(this.value) ? this.value : (!this.value ? [] : this.value.toString().split(','));
// Check any already found options
if(Object.values(this.menuItems).filter((option) => valueArray.find(val => val == option.value)).length === 0)
if(Object.values(this.getAllOptions()).filter((option) => valueArray.find(val => val == option.value)).length === 0)
{
return false;
}
@ -653,7 +636,9 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
protected _bindListeners()
{
this.addEventListener("sl-clear", this._handleClear);
this.addEventListener("sl-show", this._handleMenuShow);
this.addEventListener("sl-after-show", this._handleAfterShow);
this.addEventListener("sl-hide", this._handleMenuHide);
// Need our own change to catch the change event from search input
this.addEventListener("change", this._handleChange);
@ -673,14 +658,16 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this._searchInputNode?.removeEventListener("change", this._searchInputNode.handleChange);
this._searchInputNode?.addEventListener("change", this._handleSearchChange);
this.dropdown.querySelector('.select__label').addEventListener("change", this.handleTagEdit);
// this.dropdown.querySelector('.select__label').addEventListener("change", this.handleTagEdit);
});
}
protected _unbindListeners()
{
this.removeEventListener("sl-select", this._handleSelect);
this.removeEventListener("sl-show", this._handleMenuShow);
this.removeEventListener("sl-after-show", this._handleAfterShow);
this.removeEventListener("sl-hide", this._handleMenuHide);
this.removeEventListener("sl-clear", this._handleClear)
this.removeEventListener("change", this._handleChange);
this.removeEventListener("paste", this._handlePaste);
@ -688,7 +675,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this._searchInputNode?.removeEventListener("change", this._handleSearchChange);
}
handleMenuShow()
_handleMenuShow()
{
if(this.readonly)
{
@ -698,15 +685,11 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this._activeControls?.classList.toggle("novalue", this.multiple && this.value == '' || !this.multiple);
// Reset for parent calculations, will be adjusted after if needed
this.dropdown.setAttribute("distance", 0);
super.handleMenuShow();
//this.dropdown.setAttribute("distance", 0);
if(this.searchEnabled || this.allowFreeEntries)
{
this._activeControls?.classList.add("active");
this._searchInputNode.focus();
this._searchInputNode.select();
// Hide edit explicitly since it's so hard via CSS
if(this._editInputNode)
{
@ -729,6 +712,12 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
_handleAfterShow()
{
if(this.searchEnabled || this.allowFreeEntries)
{
this._searchInputNode.focus();
this._searchInputNode.select();
}
return;
// Need to give positioner a chance to position.
// If we call it right away, it has not updated.
// I haven't found an event or Promise to hook on to
@ -749,24 +738,22 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
);
}
}, 100);
}
focus()
{
this.dropdown?.show().then(() =>
{
this._searchInputNode.focus();
});
this.show();
this._searchInputNode.focus();
}
handleMenuHide()
_handleMenuHide()
{
if(this.readonly)
{
return;
}
clearTimeout(this._searchTimeout);
super.handleMenuHide();
this.clearSearch();
// Reset display
if(this._searchInputNode)
@ -778,11 +765,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this._editInputNode.style.display = "";
}
if(this.searchEnabled || this.allowFreeEntries)
{
this._activeControls?.classList.remove("active");
this.shadowRoot.querySelector('.select__label').style.display = "";
}
this._activeControls?.classList.remove("active");
}
_triggerChange(event)
@ -830,14 +813,14 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
/**
* An option was selected
*/
handleMenuSelect(event)
handleOptionClick(event)
{
// Need to keep the remote option - only if selected
if(event.detail.item.classList.contains("remote") && !this.select_options.find(o => o.value == event.detail.item.value))
if(event.target.classList.contains("remote") && !this.select_options.find(o => o.value == event.target.value))
{
this._selected_remote.push({...event.detail.item.option});
this._selected_remote.push({...event.target.option});
}
super.handleMenuSelect(event);
super.handleOptionClick(event);
this.updateComplete.then(() =>
{
@ -850,12 +833,12 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// If we were overlapping, reset
if(this._activeControls.classList.contains("novalue"))
{
this.handleMenuShow();
this._handleMenuShow();
this._handleAfterShow();
}
// Scroll the new tag into view
if(event.detail && event.detail.item)
if(event.detail)
{
// Causes sidemenu (calendar) to scroll to top & get stuck
/*
@ -881,17 +864,14 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
_handleClear(e)
{
// Only keep remote options that are still used
this._selected_remote = this._selected_remote.filter((option) => this.getValueAsArray().indexOf(option.value) !== -1);
this._selected_remote = this._selected_remote.filter((option) => this.value.indexOf(option.value) !== -1);
if(!this.multiple && this.searchEnabled)
{
this._handleSearchAbort(e);
// Restore label styling
this.shadowRoot.querySelector("[part='display-label']").style.display = "";
// Start searching again
this.updateComplete.then(() => this.handleMenuShow())
this._handleMenuShow();
}
}
@ -906,17 +886,6 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
async _handleSearchBlur(event : FocusEvent)
{
clearTimeout(this._searchTimeout);
if(event.relatedTarget && event.relatedTarget instanceof SlMenuItem)
{
return;
}
// Try any value they had in progress
if(this._searchInputNode.value && this.allowFreeEntries)
{
this.createFreeEntry(this._searchInputNode.value);
}
this.clearSearch();
}
/**
@ -928,15 +897,14 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
clearTimeout(this._searchTimeout);
this._activeControls?.classList.add("active");
this.dropdown.show();
// Pass off some keys to select
if(['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key))
{
// Strip out hidden non-matching selected & disabled items so key navigation works
this.menuItems = this.menuItems.filter(i => !i.disabled);
return super.handleKeyDown(event);
// TODO
return;
}
event.stopPropagation();
@ -946,12 +914,12 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
event.preventDefault();
this._searchInputNode.value = "";
this.dropdown.hide().then(async() =>
this.updateComplete.then(async() =>
{
// update sizing / position before getting ready for another one
if(this.multiple)
{
await this.dropdown.show();
// await this.show();
this._searchInputNode.focus();
}
});
@ -977,6 +945,17 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
}
/**
* Combobox listens for mousedown, which interferes with search clear button.
* Here we block it from bubbling
* @param {MouseEvent} event
* @protected
*/
protected _handleSearchMouseDown(event : MouseEvent)
{
event.stopPropagation();
}
protected _handleEditKeyDown(event : KeyboardEvent)
{
// Stop propagation, or parent key handler will add again
@ -1037,7 +1016,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// Show a spinner
let spinner = document.createElement("sl-spinner");
spinner.slot = "suffix";
spinner.slot = "expand-icon";
this.appendChild(spinner);
// Hide clear button
@ -1048,6 +1027,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
}
// Clear previous results
this._total_result_count = 0;
this._clearResults();
await this.updateComplete;
@ -1058,7 +1038,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
]).then(() =>
{
// Show no results indicator
if(this.menuItems.filter(e => !e.classList.contains("no-match")).length == 0)
if(this.getAllOptions().filter(e => !e.classList.contains("no-match")).length == 0)
{
let target = this._optionTargetNode || this;
let temp = document.createElement("div");
@ -1074,13 +1054,6 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
clear_button.style.display = "";
}
}).then(() =>
{
// Not sure why this stays hidden if there's no results, but it sticks and hides all results afterward
this.dropdown.shadowRoot.querySelector(".dropdown__panel").removeAttribute("hidden");
// Call our resize stuff explicitly
this._handleAfterShow();
});
}
@ -1128,14 +1101,9 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
n.remove();
}
})
// Reset remaining options. It might be faster to re-create instead.
this._menuItems.forEach((item) =>
{
item.disabled = item.option?.disabled || false;
item.classList.remove("match");
item.classList.remove("no-match");
});
// Not searching anymore, clear flag
this.select_options.map((o) => o.isMatch = null);
this.requestUpdate("select_options");
}
/**
@ -1148,14 +1116,11 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
return new Promise((resolve) =>
{
this.localItems.forEach((item) =>
this.select_options.forEach((option) =>
{
let match = this.searchMatch(search, item);
item.classList.toggle("match", match);
// set disabled so arrow keys step over. Might be a better way to handle that
item.disabled = !match;
item.classList.toggle("no-match", !match);
option.isMatch = this.searchMatch(search, option);
})
this.requestUpdate("select_options");
resolve();
});
}
@ -1208,13 +1173,13 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
return option.label.toLowerCase().includes(lower_search) || option.value.includes(search)
});
// Limit results
const totalCount = filtered.length;
this._total_result_count = filtered.length;
if(filtered.length > Et2WidgetWithSearch.RESULT_LIMIT)
{
filtered.splice(Et2WidgetWithSearch.RESULT_LIMIT);
}
// Add the matches
this.processRemoteResults(filtered, totalCount);
this.processRemoteResults(filtered);
return filtered;
})
.catch((_err) =>
@ -1252,14 +1217,14 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
{
// If results have a total included, pull it out.
// It will cause errors if left in the results
let total = null;
this._total_result_count = results.length;
if(typeof results.total !== "undefined")
{
total = results.total;
this._total_result_count = results.total;
delete results.total;
}
let entries = cleanSelectOptions(results);
this.processRemoteResults(entries, total);
this.processRemoteResults(entries);
return entries;
});
}
@ -1267,68 +1232,36 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
/**
* Add in remote results
* @param results
* @param totalResults If there are more results than were returned, total number of matches
* @protected
*/
protected processRemoteResults(entries, totalResults = 0)
protected processRemoteResults(entries)
{
if(!entries?.length)
{
return Promise.resolve();
}
// Add a "remote" class so we can tell these apart from any local results
entries.forEach((entry) => entry.class = (entry.class || "") + " remote");
let target = this._optionTargetNode || this;
if(target)
entries.forEach((entry) =>
{
// Add in remote options, avoiding duplicates
this.select_options.filter(function(item)
entry.class = (entry.class || "") + " remote";
// Server says it's a match
entry.isMatch = true;
});
// Add in remote options, avoiding duplicates
this.select_options.filter(function(item)
{
let i = entries.findIndex(x => (x.value == item.value));
if(i <= -1)
{
let i = entries.findIndex(x => (x.value == item.value));
if(i <= -1)
{
entries.push(item);
}
return null;
});
entries.push(item);
}
return null;
});
let options = html`${entries.map(this._optionTemplate.bind(this))}`;
/**
* Add in new options.
* Rendering directly into target will remove existing options, which we don't need to do
*/
let temp_target = document.createElement("div");
let resultCount = entries.length;
render(options, temp_target);
return Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
.then(() =>
{
temp_target.querySelectorAll(":scope > *").forEach((item) =>
{
// Avoid duplicate error
if(!target.querySelector("[value='" + ('' + item.value).replace(/'/g, "\\\'") + "']"))
{
target.appendChild(item);
}
})
this.handleMenuSlotChange();
})
.then(() =>
{
if(totalResults && totalResults > resultCount)
{
// More results available that were not sent
let count = document.createElement("span")
count.classList.add("remote");
count.textContent = this.egw().lang("%1 more...", totalResults - resultCount);
target.appendChild(count);
}
});
}
this._remote_options = entries;
this.requestUpdate("select_options");
}
/**
@ -1339,21 +1272,21 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
* @returns {boolean}
* @protected
*/
protected searchMatch(search, item) : boolean
protected searchMatch(search, option : SelectOption) : boolean
{
if(!item || !item.value)
if(!option || !option.value)
{
return false;
}
if(item.textContent?.toLowerCase().includes(search.toLowerCase()))
if(option.label?.toLowerCase().includes(search.toLowerCase()))
{
return true;
}
if(typeof item.value == "string")
if(typeof option.value == "string")
{
return item.value.includes(search.toLowerCase());
return option.value.includes(search.toLowerCase());
}
return item.value == search;
return option.value == search;
}
/**
@ -1378,16 +1311,21 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this.requestUpdate('select_options');
}
// Make sure not to double-add
if(this.multiple && this.value.indexOf(text) == -1)
// Make sure not to double-add, but wait until the option is there
this.updateComplete.then(() =>
{
this.value.push(text);
}
else if(!this.multiple && this.value !== text)
{
this.value = text;
}
this.requestUpdate("value");
if(this.multiple && this.getValueAsArray().indexOf(text) == -1)
{
let value = this.getValueAsArray();
value.push(text);
this.value = value;
}
else if(!this.multiple && this.value !== text)
{
this.value = text;
}
this.requestUpdate("value");
});
// If we were overlapping edit inputbox with the value display, reset
if(!this.readonly && this._activeControls?.classList.contains("novalue"))
@ -1478,7 +1416,7 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
// type to select will focus matching entries, but we don't want to stop the edit yet
if(typeof abort == "object" && abort.type == "blur")
{
if(abort.relatedTarget?.localName == "sl-menu-item")
if(abort.relatedTarget?.localName == this.optionTag)
{
return;
}
@ -1530,14 +1468,12 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
this.dropdown.panel.setAttribute("hidden", "");
});
}
this.syncItemsFromValue();
}
protected _handleSearchAbort(e)
{
this._activeControls.classList.remove("active");
this.clearSearch();
this.syncItemsFromValue();
}
/**
@ -1552,7 +1488,14 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
e.preventDefault();
return false;
}
protected _handleSearchClear(e)
{
e.stopImmediatePropagation();
e.preventDefault();
this.clearSearch();
}
}
return Et2WidgetWithSearch as unknown as Constructor<SearchMixinInterface> & T;
}
});

View File

@ -7,13 +7,13 @@
* @author Ralf Becker <rb@egroupware.org>
*/
import {Et2Select} from "./Et2Select";
import {cleanSelectOptions, SelectOption} from "./FindSelectOptions";
import {SelectAccountMixin} from "./SelectAccountMixin";
import {Et2StaticSelectMixin} from "./StaticOptions";
import {html, nothing} from "@lion/core";
import {Et2Select} from "../Et2Select";
import {cleanSelectOptions, SelectOption} from "../FindSelectOptions";
import {SelectAccountMixin} from "../SelectAccountMixin";
import {Et2StaticSelectMixin} from "../StaticOptions";
import {html, nothing} from "lit";
export type AccountType = 'accounts'|'groups'|'both'|'owngroups';
export type AccountType = 'accounts' | 'groups' | 'both' | 'owngroups';
/**
* @customElement et2-select-account
@ -58,17 +58,16 @@ export class Et2SelectAccount extends SelectAccountMixin(Et2StaticSelectMixin(Et
{
if(this.accountType === 'both')
{
fetch.push(this.egw().accounts('accounts').then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
fetch.push(this.egw().accounts('accounts').then(options => {this._static_options = this._static_options.concat(cleanSelectOptions(options))}));
}
fetch.push(this.egw().accounts('owngroups').then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
fetch.push(this.egw().accounts('owngroups').then(options => {this._static_options = this._static_options.concat(cleanSelectOptions(options))}));
}
else
{
fetch.push(this.egw().accounts(this.accountType).then(options => {this.static_options = this.static_options.concat(cleanSelectOptions(options))}));
fetch.push(this.egw().accounts(this.accountType).then(options => {this._static_options = this._static_options.concat(cleanSelectOptions(options))}));
}
this.fetchComplete = Promise.all(fetch)
.then(() => this._renderOptions());
this.fetchComplete = Promise.all(fetch);
}
@ -102,12 +101,7 @@ export class Et2SelectAccount extends SelectAccountMixin(Et2StaticSelectMixin(Et
{
return [];
}
let select_options : Array<SelectOption> = [...(this.static_options || []), ...super.select_options];
return select_options.filter((value, index, self) =>
{
return self.findIndex(v => v.value === value.value) === index;
});
return super.select_options;
}
set select_options(new_options : SelectOption[])

View File

@ -0,0 +1,17 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
import {cleanSelectOptions} from "../FindSelectOptions";
export class Et2SelectApp extends Et2StaticSelectMixin(Et2Select)
{
public connectedCallback()
{
super.connectedCallback()
this.fetchComplete = so.app(this, {}).then((options) =>
{
this.set_static_options(cleanSelectOptions(options));
})
}
}
customElements.define("et2-select-app", Et2SelectApp);

View File

@ -0,0 +1,26 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin} from "../StaticOptions";
export class Et2SelectBitwise extends Et2StaticSelectMixin(Et2Select)
{
/* currently handled server-side */
/*
set value(new_value)
{
let oldValue = this._value;
let expanded_value = [];
let options = this.select_options;
for(let index in options)
{
let right = parseInt(options[index].value);
if(!!(new_value & right))
{
expanded_value.push(right);
}
}
super.value = expanded_value;
}
*/
}
customElements.define("et2-select-bitwise", Et2SelectBitwise);

View File

@ -0,0 +1,28 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectBool extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.bool(this);
}
get value()
{
return super.value;
}
/**
* Boolean option values are "0" and "1", so change boolean to those
* @param {string | string[]} new_value
*/
set value(new_value)
{
super.value = new_value ? "1" : "0";
}
}
customElements.define("et2-select-bool", Et2SelectBool);

View File

@ -8,10 +8,13 @@
*/
import {css, PropertyValues} from "@lion/core";
import {Et2Select} from "./Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "./StaticOptions";
import {cleanSelectOptions} from "./FindSelectOptions";
import {css, html, nothing, PropertyValues, TemplateResult, unsafeCSS} from "lit";
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
import {cleanSelectOptions} from "../FindSelectOptions";
import {StaticValue} from "lit/development/static-html";
import {literal} from "lit/static-html.js";
import {repeat} from "lit/directives/repeat.js";
/**
* Customised Select widget for categories
@ -25,12 +28,14 @@ export class Et2SelectCategory extends Et2StaticSelectMixin(Et2Select)
...super.styles,
css`
/* Category color on options */
::slotted(*) {
sl-option {
border-left: 6px solid var(--category-color, transparent);
}
/* Border on the (single) selected value */
:host(.hasValue:not([multiple])) .select--standard .select__control {
border-left: 6px solid var(--sl-input-border-color);
:host(:not([multiple]))::part(combobox) {
border-left: 6px solid var(--category-color, var(--sl-input-border-color));
}
`
]
@ -73,13 +78,6 @@ export class Et2SelectCategory extends Et2StaticSelectMixin(Et2Select)
(this.getInstanceManager() && this.getInstanceManager().app) ||
this.egw().app_name();
}
// If app passes options (addressbook index) we'll use those instead.
// They will be found automatically by update() after ID is set.
await this.updateComplete;
if(this.select_options.length == 0)
{
}
}
@ -91,72 +89,57 @@ export class Et2SelectCategory extends Et2StaticSelectMixin(Et2Select)
{
this.fetchComplete = so.cat(this).then(options =>
{
this.static_options = cleanSelectOptions(options);
this._static_options = cleanSelectOptions(options);
this.requestUpdate("select_options");
});
}
if(changedProperties.has("value") || changedProperties.has('select_options'))
{
this.doLabelChange()
}
}
/**
* Override from parent (SlSelect) to customise display of the current value.
* Here's where we add the icon & color border
*/
doLabelChange()
protected handleValueChange(e)
{
// Update the display label when checked menu item's label changes
if(this.multiple)
{
return;
}
super.handleValueChange(e);
const checkedItem = this.menuItems.find(item => item.value === this.value);
this.displayLabel = checkedItem ? checkedItem.textContent : '';
this.querySelector("[slot=prefix].tag_image")?.remove();
if(checkedItem)
{
let image = this._createImage(checkedItem)
if(image)
{
this.append(image);
}
this.dropdown.querySelector(".select__control").style.borderColor =
getComputedStyle(checkedItem).getPropertyValue("--category-color") || "";
}
// Just re-draw to get the colors & icon
this.requestUpdate();
}
/**
* Render select_options as child DOM Nodes
* Used to render each option into the select
* Overridden for colors
*
* Overridden here so we can re-do the displayed label after first load of select options.
* Initial load order / lifecycle does not have all the options at the right time
* @protected
* @param {SelectOption} option
* @returns {TemplateResult}
*/
protected _renderOptions()
public render() : TemplateResult
{
// @ts-ignore Doesn't know about Et2WidgetWithSelectMixin._renderOptions()
return super._renderOptions().then(() =>
{
// @ts-ignore Doesn't know about SlSelect.menuItems
if(this.menuItems.length > 0)
{
this.doLabelChange();
}
});
/** CSS variables are not making it through to options, re-declaring them here works */
return html`
<style>
${repeat(this.select_options, (option) =>
{
if(typeof option.color == "undefined" || !option.color)
{
return nothing;
}
return unsafeCSS(
(this.getValueAsArray().includes(option.value) ? "::part(combobox) { --category-color: " + option.color + ";}" : "") +
".cat_" + option.value + " {--category-color: " + option.color + ";}"
);
})}
</style>
${super.render()}
`;
}
/**
* Use a custom tag for when multiple=true
*
* @returns {string}
*/
get tagTag() : string
public get tagTag() : StaticValue
{
return "et2-category-tag";
return literal`et2-category-tag`;
}
/**

View File

@ -8,10 +8,10 @@
*/
import {Et2Select} from "./Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "./StaticOptions";
import {egw} from "../../jsapi/egw_global";
import {SelectOption} from "./FindSelectOptions";
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
import {egw} from "../../../jsapi/egw_global";
import {SelectOption} from "../FindSelectOptions";
/**
* Customised Select widget for countries
@ -38,7 +38,7 @@ export class Et2SelectCountry extends Et2StaticSelectMixin(Et2Select)
(<Promise<SelectOption[]>>so.country(this, {}, true)).then(options =>
{
this.static_options = options
this._static_options = options
this.requestUpdate("select_options");
});
}

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions as so} from "../StaticOptions";
export class Et2SelectDay extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = so.day(this, {});
}
}
customElements.define("et2-select-day", Et2SelectDay);

View File

@ -0,0 +1,53 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
import {cleanSelectOptions} from "../FindSelectOptions";
export class Et2SelectDayOfWeek extends Et2StaticSelectMixin(Et2Select)
{
connectedCallback()
{
super.connectedCallback();
// Wait for connected instead of constructor because attributes make a difference in
// which options are offered
this.fetchComplete = StaticOptions.dow(this, {other: this.other || []}).then(options =>
{
this.set_static_options(cleanSelectOptions(options));
});
}
set value(new_value)
{
let expanded_value = typeof new_value == "object" ? new_value : [];
if(new_value && (typeof new_value == "string" || typeof new_value == "number"))
{
let int_value = parseInt(new_value);
this.updateComplete.then(() =>
{
this.fetchComplete.then(() =>
{
let options = this.select_options;
for(let index in options)
{
let right = parseInt(options[index].value);
if((int_value & right) == right)
{
expanded_value.push("" + right);
}
}
super.value = expanded_value;
})
});
return;
}
super.value = expanded_value;
}
get value()
{
return super.value;
}
}
customElements.define("et2-select-dow", Et2SelectDayOfWeek);

View File

@ -7,11 +7,12 @@
* @author Nathan Gray
*/
import {Et2Select} from "./Et2Select";
import {css, html, nothing, PropertyValues} from "@lion/core";
import {IsEmail} from "../Validators/IsEmail";
import {Et2Select} from "../Et2Select";
import {css, html, nothing, PropertyValues} from "lit";
import {IsEmail} from "../../Validators/IsEmail";
import interact from "@interactjs/interact";
import {Validator} from "@lion/form-core";
import {classMap} from "lit/directives/class-map.js";
/**
* Select email address(es)
@ -100,6 +101,7 @@ export class Et2SelectEmail extends Et2Select
this.defaultValidators.push(new IsEmail(this.allowPlaceholder));
}
/** @param {import('@lion/core').PropertyValues } changedProperties */
willUpdate(changedProperties : PropertyValues)
{
@ -112,6 +114,40 @@ export class Et2SelectEmail extends Et2Select
}
}
updated(changedProperties : Map<string, any>)
{
// Make tags draggable
if(!this.readonly && this.allowFreeEntries && this.allowDragAndDrop)
{
let dragTranslate = {x: 0, y: 0};
const tags = this.shadowRoot.querySelectorAll(".select__tags [part='tag']");
let draggable = interact(tags).draggable({
startAxis: 'xy',
listeners: {
start: function(e)
{
let dragPosition = {x: e.page.x, y: e.page.y};
dragTranslate = {x: 0, y: 0};
e.target.setAttribute('style', `width:${e.target.clientWidth}px !important`);
e.target.style.position = 'fixed';
e.target.style.zIndex = 10;
e.target.style.transform =
`translate(${dragPosition.x}px, ${dragPosition.y}px)`;
},
move: function(e)
{
dragTranslate.x += e.delta.x;
dragTranslate.y += e.delta.y;
e.target.style.transform =
`translate(${dragTranslate.x}px, ${dragTranslate.y}px)`;
}
}
});
// set parent_node with widget context in order to make it accessible after drop
draggable.parent_node = this;
}
}
connectedCallback()
{
super.connectedCallback();
@ -145,25 +181,7 @@ export class Et2SelectEmail extends Et2Select
}
});
}
/**
* Handle keypresses inside the search input
* Overridden from parent to also skip the hidden selected options, which other selects do not do
*
* @param {KeyboardEvent} event
* @protected
*/
protected _handleSearchKeyDown(event : KeyboardEvent)
{
// Pass off some keys to select
if(['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key))
{
// Strip out hidden non-matching selected so key navigation works
this.menuItems = this.menuItems.filter(i => !i.checked);
}
return super._handleSearchKeyDown(event);
}
/**
* Actually query the server.
*
@ -187,55 +205,21 @@ export class Et2SelectEmail extends Et2Select
*
* @returns {string}
*/
get tagTag() : string
_tagTemplate(option, index)
{
return "et2-email-tag";
}
/**
* override tag creation in order to add DND functionality
* @param item
* @protected
*/
protected _createTagNode(item)
{
let tag = super._createTagNode(item);
tag.fullEmail = this.fullEmail;
tag.onlyEmail = this.onlyEmail;
// Re-set after setting fullEmail as that can change what we show
tag.textContent = item.getTextLabel().trim();
if(!this.readonly && this.allowFreeEntries && this.allowDragAndDrop)
{
let dragTranslate = {x: 0, y: 0};
tag.class = item.classList.value + " et2-select-draggable";
let draggable = interact(tag).draggable({
startAxis: 'xy',
listeners: {
start: function(e)
{
let dragPosition = {x:e.page.x, y:e.page.y};
e.target.setAttribute('style', `width:${e.target.clientWidth}px !important`);
e.target.style.position = 'fixed';
e.target.style.zIndex = 10;
e.target.style.transform =
`translate(${dragPosition.x}px, ${dragPosition.y}px)`;
},
move : function(e)
{
dragTranslate.x += e.delta.x;
dragTranslate.y += e.delta.y;
e.target.style.transform =
`translate(${dragTranslate.x}px, ${dragTranslate.y}px)`;
}
}
});
// set parent_node with widget context in order to make it accessible after drop
draggable.parent_node = this;
}
return tag;
return html`
<et2-email-tag
class=${classMap({
...option.classList,
"et2-select-draggable": !this.readonly && this.allowFreeEntries && this.allowDragAndDrop
})}
?.fullEmail=${this.fullEmail}
?.onlyEmail=${this.onlyEmail}
value=${option.value}
>
${option.getTextLabel().trim()}
</et2-email-tag>
`;
}
/**

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectHour extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.hour(this, {other: this.other || []});
}
}
customElements.define("et2-select-hour", Et2SelectHour);

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectLang extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.lang(this, {other: this.other || []});
}
}
customElements.define("et2-select-lang", Et2SelectLang);

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectMonth extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.month(this);
}
}
customElements.define("et2-select-month", Et2SelectMonth);

View File

@ -0,0 +1,52 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
import {PropertyValues} from 'lit';
export class Et2SelectNumber extends Et2StaticSelectMixin(Et2Select)
{
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}
}
}
constructor()
{
super();
this.min = 1;
this.max = 10;
this.interval = 1;
this.leading_zero = "";
this.suffix = "";
}
updated(changedProperties : PropertyValues)
{
super.updated(changedProperties);
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
{
this._static_options = StaticOptions.number(this);
this.requestUpdate("select_options");
}
}
}
customElements.define("et2-select-number", Et2SelectNumber);

View File

@ -0,0 +1,15 @@
import {Et2SelectNumber} from "./Et2SelectNumber";
export class Et2SelectPercent extends Et2SelectNumber
{
constructor()
{
super();
this.min = 0;
this.max = 100;
this.interval = 10;
this.suffix = "%%";
}
}
customElements.define("et2-select-percent", Et2SelectPercent);

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectPriority extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.priority(this);
}
}
customElements.define("et2-select-priority", Et2SelectPriority);

View File

@ -8,12 +8,13 @@
*/
import {css, html, LitElement, repeat, TemplateResult} from "@lion/core";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {Et2StaticSelectMixin, StaticOptions, StaticOptions as so} from "./StaticOptions";
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
import {SelectAccountMixin} from "./SelectAccountMixin";
import {css, html, LitElement, TemplateResult} from "lit";
import {repeat} from "lit/directives/repeat.js";
import {et2_IDetachedDOM} from "../../et2_core_interfaces";
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {Et2StaticSelectMixin, StaticOptions, StaticOptions as so} from "../StaticOptions";
import {cleanSelectOptions, find_select_options, SelectOption} from "../FindSelectOptions";
import {SelectAccountMixin} from "../SelectAccountMixin";
/**
* This is a stripped-down read-only widget used in nextmatch
@ -143,6 +144,11 @@ li {
return this.value;
}
getValueAsArray()
{
return (Array.isArray(this.value) ? this.value : [this.value]);
}
set value(new_value : string | string[])
{
// Split anything that is still a CSV
@ -206,10 +212,11 @@ li {
render()
{
const value = this.getValueAsArray();
return html`
<ul>
${repeat(
(Array.isArray(this.value) ? this.value : [this.value]),
this.getValueAsArray(),
(val : string) => val, (val) =>
{
let option = (<SelectOption[]>this.select_options).find(option => option.value == val);
@ -282,14 +289,16 @@ customElements.define("et2-select-app_ro", Et2SelectAppReadonly);
export class Et2SelectBitwiseReadonly extends Et2SelectReadonly
{
/* Currently handled server side, we get an array
render()
{
let new_value = [];
let int_value = parseInt(this.value);
for(let index in this.select_options)
{
let option = this.select_options[index];
let right = parseInt(option && option.value ? option.value : index);
if(!!(this.value & right))
if(!!(int_value & right))
{
new_value.push(right);
}
@ -307,6 +316,8 @@ export class Et2SelectBitwiseReadonly extends Et2SelectReadonly
})}
</ul>`;
}
*/
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
@ -349,7 +360,6 @@ export class Et2SelectPercentReadonly extends Et2SelectReadonly
constructor()
{
super(...arguments);
this.suffix = "%%";
this.select_options = so.percent(this);
}
}
@ -391,6 +401,23 @@ export class Et2SelectDayOfWeekReadonly extends Et2StaticSelectMixin(Et2SelectRe
this.set_static_options(cleanSelectOptions(options));
});
}
getValueAsArray()
{
let expanded_value = [];
let int_value = parseInt(this.value);
let options = this.select_options;
for(let index in options)
{
let right = parseInt(<string>options[index].value);
if((int_value & right) == right)
{
expanded_value.push("" + right);
}
}
return expanded_value;
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
@ -422,7 +449,7 @@ export class Et2SelectNumberReadonly extends Et2StaticSelectMixin(Et2SelectReado
{
protected find_select_options(_attrs)
{
this.static_options = so.number(this, _attrs);
this._static_options = so.number(this, _attrs);
}
}

View File

@ -0,0 +1,45 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
import {SelectOption} from "../FindSelectOptions";
export class Et2SelectState extends Et2StaticSelectMixin(Et2Select)
{
/**
* Two-letter ISO country code
*/
protected __countryCode;
static get properties()
{
return {
...super.properties,
countryCode: String,
}
}
constructor()
{
super();
this.countryCode = 'DE';
}
get countryCode()
{
return this.__countryCode;
}
set countryCode(code : string)
{
this.__countryCode = code;
this._static_options = <SelectOption[]>StaticOptions.state(this, {country_code: code});
this.requestUpdate("select_options");
}
set_country_code(code)
{
this.countryCode = code;
}
}
customElements.define("et2-select-state", Et2SelectState);

View File

@ -0,0 +1,61 @@
import {Et2SelectApp} from "./Et2SelectApp";
import {SelectOption} from "../FindSelectOptions";
export class Et2SelectTab extends Et2SelectApp
{
constructor()
{
super();
this.allowFreeEntries = true;
}
set value(new_value)
{
if(!new_value)
{
super.value = new_value;
return;
}
const values = Array.isArray(new_value) ? new_value : [new_value];
const options = this.select_options;
values.forEach(value =>
{
if(!options.filter(option => option.value == value).length)
{
const matches = value.match(/^([a-z0-9]+)\-/i);
let option : SelectOption = {value: value, label: value};
if(matches)
{
option = options.filter(option => option.value == matches[1])[0] || {
value: value,
label: this.egw().lang(matches[1])
};
option.value = value;
option.label += ' ' + this.egw().lang('Tab');
}
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;
}
}
customElements.define("et2-select-tab", Et2SelectTab);

View File

@ -7,9 +7,9 @@
* @author Nathan Gray
*/
import {Et2Select} from "./Et2Select";
import {css} from "@lion/core";
import {SelectOption} from "./FindSelectOptions";
import {Et2Select} from "../Et2Select";
import {css} from "lit";
import {SelectOption} from "../FindSelectOptions";
export class Et2SelectThumbnail extends Et2Select
{

View File

@ -0,0 +1,14 @@
import {Et2Select} from "../Et2Select";
import {Et2StaticSelectMixin, StaticOptions} from "../StaticOptions";
export class Et2SelectTimezone extends Et2StaticSelectMixin(Et2Select)
{
constructor()
{
super();
this._static_options = StaticOptions.timezone(this, {other: this.other || []});
}
}
customElements.define("et2-select-timezone", Et2SelectTimezone);

View File

@ -0,0 +1,25 @@
import {Et2SelectNumber} from "./Et2SelectNumber";
import {PropertyValues} from "lit";
import {StaticOptions} from "../StaticOptions";
export class Et2SelectYear extends Et2SelectNumber
{
constructor()
{
super();
this.min = -3;
this.max = 2;
}
updated(changedProperties : PropertyValues)
{
super.updated(changedProperties);
if(changedProperties.has('min') || changedProperties.has('max') || changedProperties.has('interval') || changedProperties.has('suffix'))
{
this._static_options = StaticOptions.year(this);
}
}
}
customElements.define("et2-select-year", Et2SelectYear);

View File

@ -1,5 +1,5 @@
import {SelectOption} from "./FindSelectOptions";
import {LitElement} from "@lion/core";
import {LitElement} from "lit";
/**
* EGroupware eTemplate2 - SelectAccountMixin

View File

@ -0,0 +1,25 @@
/**
* Import all our sub-types
*/
import './Select/Et2SelectAccount';
import './Select/Et2SelectApp';
import './Select/Et2SelectBitwise';
import './Select/Et2SelectBool';
import './Select/Et2SelectCategory';
import './Select/Et2SelectCountry';
import './Select/Et2SelectDay';
import './Select/Et2SelectDayOfWeek';
import './Select/Et2SelectEmail';
import './Select/Et2SelectHour';
import './Select/Et2SelectLang';
import './Select/Et2SelectMonth';
import './Select/Et2SelectNumber';
import './Select/Et2SelectPercent';
import './Select/Et2SelectPriority';
import './Select/Et2SelectReadonly';
import './Select/Et2SelectState';
import './Select/Et2SelectTab';
import './Select/Et2SelectThumbnail';
import './Select/Et2SelectTimezone';
import './Select/Et2SelectYear';

View File

@ -0,0 +1,285 @@
/**
* 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 { sprintf } from "../../egw_action/egw_action_common";
import { cleanSelectOptions, find_select_options } from "./FindSelectOptions";
/**
* Base class for things that have static options
*
* We keep static options separate and concatenate them in to allow for extra options without
* overwriting them when we get static options from the server
*/
export const Et2StaticSelectMixin = (superclass) => {
class Et2StaticSelectOptions extends (superclass) {
constructor(...args) {
super(...args);
this.static_options = [];
this.fetchComplete = Promise.resolve();
// Trigger the options to get rendered into the DOM
this.requestUpdate("select_options");
}
get select_options() {
// @ts-ignore
const options = super.select_options || [];
// make sure result is unique
return [...new Map([...(this.static_options || []), ...options].map(item => [item.value, item])).values()];
}
set select_options(new_options) {
// @ts-ignore IDE doesn't recognise property
super.select_options = new_options;
}
set_static_options(new_static_options) {
this.static_options = new_static_options;
this.requestUpdate("select_options");
}
/**
* Override the parent fix_bad_value() to wait for server-side options
* to come back before we check to see if the value is not there.
*/
fix_bad_value() {
this.fetchComplete.then(() => {
// @ts-ignore Doesn't know it's an Et2Select
if (typeof super.fix_bad_value == "function") {
// @ts-ignore Doesn't know it's an Et2Select
super.fix_bad_value();
}
});
}
}
return Et2StaticSelectOptions;
};
/**
* 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)
* @param {boolean} return_promise true: always return a promise
* @returns {Object[]|Promise<Object[]>} Array of options, or empty and they'll get filled in later, or Promise
*/
export const StaticOptions = new class StaticOptionsType {
cached_server_side(widget, type, options_string, return_promise) {
// normalize options by removing trailing commas
options_string = options_string.replace(/,+$/, '');
const cache_id = widget.nodeName + '_' + 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 = widget.egw().json('EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', [type, options_string, widget.value]).sendRequest();
if (typeof cache === 'undefined') {
cache_owner[cache_id] = req;
}
cache = req;
}
if (typeof cache.then === 'function') {
// pending, wait for it
const promise = cache.then((response) => {
cache = cache_owner[cache_id] = response.response[0].data || undefined;
if (return_promise)
return cache;
// Set select_options in attributes in case we get a response before
// the widget is finished loading (otherwise it will re-set to {})
//widget.select_options = cache;
// Avoid errors if widget is destroyed before the timeout
if (widget && typeof widget.id !== 'undefined') {
if (typeof widget.set_static_options == "function") {
widget.set_static_options(cache);
}
else if (typeof widget.set_select_options == "function") {
widget.set_select_options(find_select_options(widget, {}, cache));
}
}
});
return return_promise ? promise : [];
}
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 (widget.value && widget.value != "" && widget.value != "0" && type == "select-timezone") {
var missing_option = true;
for (var i = 0; i < cache.length && missing_option; i++) {
if (cache[i].value == widget.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, type, options_string);
}
else {
if (widget.value && widget && widget.get_value() !== widget.value) {
egw.window.setTimeout(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);
}
}.bind({ widget: widget }), 1);
}
}
}
return return_promise ? Promise.resolve(cache) : cache;
}
}
cached_from_file(widget, file) {
const cache_owner = widget.egw().getCache('Et2Select');
let cache = cache_owner[file];
if (typeof cache === 'undefined') {
cache_owner[file] = cache = widget.egw().window.fetch(file)
.then((response) => {
// Get the options
if (!response.ok) {
throw response;
}
return response.json();
})
.then(options => {
var _a;
// Need to clean the options because file may be key=>value, may have option list, may be mixed
cache_owner[file] = (_a = cleanSelectOptions(options)) !== null && _a !== void 0 ? _a : [];
return cache_owner[file];
});
}
else if (cache && typeof cache.then === "undefined") {
return Promise.resolve(cache);
}
return cache;
}
priority(widget) {
return [
{ value: "1", label: 'low' },
{ value: "2", label: 'normal' },
{ value: "3", label: 'high' },
{ value: "0", label: 'undefined' }
];
}
bool(widget) {
return [
{ value: "0", label: 'no' },
{ value: "1", label: 'yes' }
];
}
month(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(widget, attrs = {
min: undefined,
max: undefined,
interval: undefined,
format: undefined
}) {
var _a, _b, _c, _d;
var options = [];
var min = (_a = attrs.min) !== null && _a !== void 0 ? _a : parseFloat(widget.min);
var max = (_b = attrs.max) !== null && _b !== void 0 ? _b : parseFloat(widget.max);
var interval = (_c = attrs.interval) !== null && _c !== void 0 ? _c : parseFloat(widget.interval);
var format = (_d = attrs.format) !== null && _d !== void 0 ? _d : '%d';
// leading zero specified in interval
if (widget.leading_zero && widget.leading_zero[0] == '0') {
format = '%0' + ('' + interval).length + 'd';
}
// Suffix
if (widget.suffix) {
format += widget.egw().lang(widget.suffix);
}
// 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) {
return this.number(widget);
}
year(widget, attrs) {
if (typeof attrs != 'object') {
attrs = {};
}
var t = new Date();
attrs.min = t.getFullYear() + parseInt(widget.min);
attrs.max = t.getFullYear() + parseInt(widget.max);
return this.number(widget, attrs);
}
day(widget, attrs) {
attrs.other = [1, 31, 1];
return this.number(widget, attrs);
}
hour(widget, attrs) {
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, attrs) {
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, 'select-app', options);
}
cat(widget) {
var options = [widget.globalCategories, /*?*/ , widget.application, widget.parentCat];
if (typeof options[3] == 'undefined') {
options[3] = widget.application ||
// When the widget is first created, it doesn't have a parent and can't find it's instanceManager
(widget.getInstanceManager() && widget.getInstanceManager().app) ||
widget.egw().app_name();
}
return this.cached_server_side(widget, 'select-cat', options.join(','), true);
}
country(widget, attrs, return_promise) {
var options = ',';
return this.cached_server_side(widget, 'select-country', options, return_promise);
}
state(widget, attrs) {
var options = attrs.country_code ? attrs.country_code : 'de';
return this.cached_server_side(widget, 'select-state', options);
}
dow(widget, attrs) {
var options = (widget.rows || "") + ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, 'select-dow', options, true);
}
lang(widget, attrs) {
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, 'select-lang', options);
}
timezone(widget, attrs) {
var options = ',' + (attrs.other || []).join(',');
return this.cached_server_side(widget, 'select-timezone', options);
}
};
//# sourceMappingURL=StaticOptions.js.map

View File

@ -8,11 +8,18 @@
* @param {type} widget
*/
import {sprintf} from "../../egw_action/egw_action_common";
import {Et2SelectReadonly} from "./Et2SelectReadonly";
import {Et2SelectReadonly} from "./Select/Et2SelectReadonly";
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
import {Et2Select, Et2WidgetWithSelect} from "./Et2Select";
import {state} from "lit/decorators/state.js";
export type Et2SelectWidgets = Et2Select | Et2WidgetWithSelect | Et2SelectReadonly;
type NumberOptions = {
min? : number,
max? : number,
interval? : number,
format? : string
};
// Export the Interface for TypeScript
type Constructor<T = {}> = new (...args : any[]) => T;
@ -31,27 +38,25 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
// Hold the static widget options separately so other options (like sent from server in sel_options) won't
// conflict or be wiped out
protected static_options : SelectOption[];
@state()
protected _static_options : SelectOption[] = [];
// If widget needs to fetch options from server, we might want to wait for them
protected fetchComplete : Promise<SelectOption[] | void>;
@state()
protected fetchComplete : Promise<SelectOption[] | void> = Promise.resolve();
constructor(...args)
async getUpdateComplete() : Promise<boolean>
{
super(...args);
this.static_options = [];
this.fetchComplete = Promise.resolve();
// Trigger the options to get rendered into the DOM
this.requestUpdate("select_options");
const result = await super.getUpdateComplete();
await this.fetchComplete;
return result;
}
get select_options() : SelectOption[]
{
// @ts-ignore
const options = super.select_options || [];
const statics = this.static_options || [];
const statics = this._static_options || [];
if(options.length == 0)
{
@ -62,7 +67,7 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
return options;
}
// Merge & make sure result is unique
return [...new Map([...(this.static_options || []), ...options].map(item =>
return [...new Map([...(this._static_options || []), ...options].map(item =>
[item.value, item])).values()];
}
@ -75,7 +80,7 @@ export const Et2StaticSelectMixin = <T extends Constructor<Et2WidgetWithSelect>>
set_static_options(new_static_options)
{
this.static_options = new_static_options;
this._static_options = new_static_options;
this.requestUpdate("select_options");
}
@ -273,19 +278,14 @@ export const StaticOptions = new class StaticOptionsType
];
}
number(widget : Et2SelectWidgets, attrs = {
min: undefined,
max: undefined,
interval: undefined,
format: undefined
}) : SelectOption[]
number(widget : Et2SelectWidgets, attrs : NumberOptions = {}) : SelectOption[]
{
var options = [];
var min = parseFloat(attrs.min ?? widget.min ?? 1);
var max = parseFloat(attrs.max ?? widget.max ?? 10);
var interval = parseFloat(attrs.interval ?? widget.interval ?? 1);
var format = attrs.format ?? '%d';
const options = [];
const min = parseFloat(attrs.min ?? widget.min ?? 1);
const max = parseFloat(attrs.max ?? widget.max ?? 10);
let interval = parseFloat(attrs.interval ?? widget.interval ?? 1);
let format = attrs.format ?? '%d';
// leading zero specified in interval
if(widget.leading_zero && widget.leading_zero[0] == '0')
@ -313,7 +313,7 @@ export const StaticOptions = new class StaticOptionsType
percent(widget : Et2SelectWidgets) : SelectOption[]
{
return this.number(widget, {min: 0, max: 100, interval: 10, format: undefined});
return this.number(widget, {min: 0, max: 100, interval: 10, format: "%d%%"});
}
year(widget : Et2SelectWidgets, attrs?) : SelectOption[]
@ -323,15 +323,14 @@ export const StaticOptions = new class StaticOptionsType
attrs = {}
}
var t = new Date();
attrs.min = t.getFullYear() + parseInt(widget.min);
attrs.max = t.getFullYear() + parseInt(widget.max);
attrs.min = t.getFullYear() + parseInt(attrs.min ?? widget.min ?? -3);
attrs.max = t.getFullYear() + parseInt(attrs.max ?? widget.max ?? 2);
return this.number(widget, attrs);
}
day(widget : Et2SelectWidgets, attrs) : SelectOption[]
{
attrs.other = [1, 31, 1];
return this.number(widget, attrs);
return this.number(widget, {min: 1, max: 31, interval: 1});
}
hour(widget : Et2SelectWidgets, attrs) : SelectOption[]

View File

@ -6,7 +6,7 @@
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css, html, TemplateResult} from "@lion/core";
import {css, html, TemplateResult} from "lit";
import shoelace from "../../Styles/shoelace";
import {Et2Tag} from "./Et2Tag";

View File

@ -6,7 +6,8 @@
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {classMap, css, html, nothing, PropertyValues, TemplateResult} from "@lion/core";
import {css, html, nothing, PropertyValues, TemplateResult} from "lit";
import {classMap} from "lit/directives/class-map.js";
import shoelace from "../../Styles/shoelace";
import {Et2Tag} from "./Et2Tag";
@ -95,8 +96,8 @@ export class Et2EmailTag extends Et2Tag
this.onlyEmail = false;
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleContactClick = this.handleContactClick.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleContactMouseDown = this.handleContactMouseDown.bind(this);
}
connectedCallback()
@ -166,7 +167,7 @@ export class Et2EmailTag extends Et2Tag
this.shadowRoot.querySelector(".tag").classList.remove("contact_plus");
}
handleClick(e : MouseEvent)
handleMouseDown(e : MouseEvent)
{
e.stopPropagation();
@ -177,7 +178,7 @@ export class Et2EmailTag extends Et2Tag
this.egw().open('', 'addressbook', 'add', extra);
}
handleContactClick(e : MouseEvent)
handleContactMouseDown(e : MouseEvent)
{
e.stopPropagation();
this.checkContact(this.value).then((result) =>
@ -255,7 +256,7 @@ export class Et2EmailTag extends Et2Tag
button_or_avatar = html`
<et2-lavatar slot="prefix" part="icon"
@click=${this.handleContactClick}
@mousedown=${this.handleContactMouseDown}
.size=${style.getPropertyValue("--icon-width")}
lname=${option.lname || nothing}
fname=${option.fname || nothing}
@ -269,7 +270,7 @@ export class Et2EmailTag extends Et2Tag
// Show a button to add as new contact
classes['tag__has_plus'] = true;
button_or_avatar = html`
<et2-button-icon image="add" @click=${this.handleClick}
<et2-button-icon image="add" @mousedown=${this.handleMouseDown}
label="${this.egw().lang("Add a new contact")}"
statustext="${this.egw().lang("Add a new contact")}">
</et2-button-icon>`;

View File

@ -8,7 +8,8 @@
*/
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {SlTag} from "@shoelace-style/shoelace";
import {classMap, css, html, TemplateResult} from "@lion/core";
import {css, html, TemplateResult} from "lit";
import {classMap} from "lit/directives/class-map.js";
import shoelace from "../../Styles/shoelace";
/**
@ -23,7 +24,6 @@ export class Et2Tag extends Et2Widget(SlTag)
shoelace, css`
:host {
flex: 1 1 auto;
overflow: hidden;
}
.tag--pill {
@ -113,11 +113,12 @@ export class Et2Tag extends Et2Widget(SlTag)
<sl-icon-button
part="remove-button"
exportparts="base:remove-button__base"
name="x"
name="x-lg"
library="system"
label=${this.egw().lang('remove')}
class="tag__remove"
@click=${this.handleRemoveClick}
tabindex="-1"
></sl-icon-button>
`
: ''}

View File

@ -6,7 +6,7 @@
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css} from "@lion/core";
import {css} from "lit";
import shoelace from "../../Styles/shoelace";
import {Et2Tag} from "./Et2Tag";

View File

@ -20,8 +20,8 @@ async function before(editable = true)
// @ts-ignore
element = await fixture<Et2Select>(html`
<et2-select label="I'm a select" value="one" multiple="true" .editModeEnabled=${editable}>
<sl-menu-item value="one">One</sl-menu-item>
<sl-menu-item value="two">Two</sl-menu-item>
<sl-option value="one">One</sl-option>
<sl-option value="two">Two</sl-option>
</et2-select>
`);
// Stub egw()

View File

@ -57,7 +57,7 @@ describe('Et2EmailTag', () =>
assert.equal(extra['presets[email]'], 'test@example.com');
}
});
component.handleClick(new MouseEvent('click'));
component.handleMouseDown(new MouseEvent('click'));
});
it('should open addressbook CRM on avatar click', async() =>
@ -79,6 +79,6 @@ describe('Et2EmailTag', () =>
assert.deepEqual(extra, {title: contact.n_fn, icon: contact.photo});
}
});
await component.handleContactClick(new MouseEvent('click'));
await component.handleContactMouseDown(new MouseEvent('click'));
});
});

View File

@ -97,8 +97,8 @@ describe("Multiple", () =>
// @ts-ignore
element = await fixture<Et2Select>(html`
<et2-select label="I'm a select" multiple="true">
<sl-menu-item value="one">One</sl-menu-item>
<sl-menu-item value="two">Two</sl-menu-item>
<sl-option value="one">One</sl-option>
<sl-option value="two">Two</sl-option>
</et2-select>
`);
element.set_value("one,two");
@ -111,7 +111,7 @@ describe("Multiple", () =>
it("Can remove tags", async() =>
{
assert.equal(element.querySelectorAll("sl-menu-item").length, 2, "Did not find options");
assert.equal(element.querySelectorAll("sl-option").length, 2, "Did not find options");
assert.sameMembers(element.value, ["one", "two"]);
let tags = element.shadowRoot.querySelectorAll('.select__tags > *');

View File

@ -86,7 +86,7 @@ describe("Select widget", () =>
await element.updateComplete;
/** TESTING **/
assert.equal(element.querySelectorAll("sl-menu-item").length, 2);
assert.equal(element.querySelectorAll("sl-option").length, 2);
});
it("merges static options with sel_options", async() =>
@ -109,7 +109,7 @@ describe("Select widget", () =>
/** TESTING **/
// @ts-ignore o.value isn't known by TypeScript, but it's there
let option_keys = Object.values(element.querySelectorAll("sl-menu-item")).map(o => o.value);
let option_keys = Object.values(element.querySelectorAll("sl-option")).map(o => o.value);
assert.include(option_keys, "option", "Static option missing");
assert.includeMembers(option_keys, ["1", "2", "option"], "Option mis-match");
assert.equal(option_keys.length, 3);

View File

@ -97,13 +97,13 @@ describe("Trigger search", () =>
// @ts-ignore
element = await fixture<Et2Select>(html`
<et2-select label="I'm a select" search=true>
<sl-menu-item value="one">One</sl-menu-item>
<sl-menu-item value="two">Two</sl-menu-item>
<sl-menu-item value="three">Three</sl-menu-item>
<sl-menu-item value="four">Four</sl-menu-item>
<sl-menu-item value="five">Five</sl-menu-item>
<sl-menu-item value="six">Six</sl-menu-item>
<sl-menu-item value="seven">Seven</sl-menu-item>
<sl-option value="one">One</sl-option>
<sl-option value="two">Two</sl-option>
<sl-option value="three">Three</sl-option>
<sl-option value="four">Four</sl-option>
<sl-option value="five">Five</sl-option>
<sl-option value="six">Six</sl-option>
<sl-option value="seven">Seven</sl-option>
</et2-select>
`);
// Stub egw()

View File

@ -11,7 +11,7 @@
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {SlSpinner} from "@shoelace-style/shoelace";
import shoelace from "../Styles/shoelace";
import {css} from "@lion/core";
import {css} from "lit";
export class Et2Spinner extends Et2Widget(SlSpinner)
{

View File

@ -8,8 +8,8 @@
* @author Hadi Nategh
*/
import {css, html, SlotMixin} from "@lion/core";
import {css, html} from "lit";
import {SlotMixin} from "@lion/core";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import '../Et2Image/Et2Image';
import {SlSwitch} from "@shoelace-style/shoelace";

View File

@ -9,7 +9,7 @@
*/
import {css} from "@lion/core";
import {css} from "lit";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {SlTextarea} from "@shoelace-style/shoelace";
import shoelace from "../Styles/shoelace";

View File

@ -9,7 +9,7 @@
*/
import {Et2Textbox} from "./Et2Textbox";
import {css, html, render} from "@lion/core";
import {css, html, render} from "lit";
export class Et2Number extends Et2Textbox
{

View File

@ -11,7 +11,9 @@
import {Et2InvokerMixin} from "../Et2Url/Et2InvokerMixin";
import {Et2Textbox} from "./Et2Textbox";
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
import {classMap, html, ifDefined} from "@lion/core";
import {html} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {ifDefined} from "lit/directives/if-defined.js";
import {egw} from "../../jsapi/egw_global";
const isChromium = navigator.userAgentData?.brands.some(b => b.brand.includes('Chromium'));

View File

@ -9,7 +9,7 @@
*/
import {css, PropertyValues} from "@lion/core";
import {css, PropertyValues} from "lit";
import {Regex} from "../Validators/Regex";
import {SlInput} from "@shoelace-style/shoelace";
import shoelace from "../Styles/shoelace";

View File

@ -8,7 +8,8 @@
*/
/* eslint-disable import/no-extraneous-dependencies */
import {css, dedupeMixin, html, LitElement, SlotMixin} from '@lion/core';
import {css, html, LitElement} from 'lit';
import {dedupeMixin, SlotMixin} from '@lion/core';
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
import {colorsDefStyles} from "../Styles/colorsDefStyles";

View File

@ -11,7 +11,7 @@
import {Et2InvokerMixin} from "./Et2InvokerMixin";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {css} from "@lion/core";
import {css} from "lit";
import {egw} from "../../jsapi/egw_global";
/**

View File

@ -12,7 +12,7 @@ import {Et2InvokerMixin} from "./Et2InvokerMixin";
import {IsEmail} from "../Validators/IsEmail";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {css} from "@lion/core";
import {css} from "lit";
import {egw} from "../../jsapi/egw_global";
/**

View File

@ -11,7 +11,7 @@
import {Et2UrlPhone} from "./Et2UrlPhone";
import {Et2UrlEmail} from "./Et2UrlEmail";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {css} from "@lion/core";
import {css} from "lit";
/**
* @customElement et2-url-phone

View File

@ -11,7 +11,7 @@
import {Et2InvokerMixin} from "./Et2InvokerMixin";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {css} from "@lion/core";
import {css} from "lit";
/**
* @customElement et2-url-phone

View File

@ -9,7 +9,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import {Et2Description} from "../Et2Description/Et2Description";
import {css, TemplateResult} from "@lion/core";
import {css, TemplateResult} from "lit";
import {Et2Url} from "./Et2Url";
/**

View File

@ -1,7 +1,7 @@
import {ExposeValue} from "../Expose/ExposeMixin";
import {et2_vfsMode} from "../et2_widget_vfs";
import {Et2ImageExpose} from "../Expose/Et2ImageExpose";
import {css, html} from "@lion/core";
import {css, html} from "lit";
export class Et2VfsMime extends Et2ImageExpose

View File

@ -7,7 +7,7 @@
* @author Ralf Becker <rb@egroupware.org>
*/
import {Et2SelectAccountReadonly} from "../Et2Select/Et2SelectReadonly";
import {Et2SelectAccountReadonly} from "../Et2Select/Select/Et2SelectReadonly";
export class Et2VfsUid extends Et2SelectAccountReadonly
{

View File

@ -8,7 +8,8 @@ import {et2_cloneObject, et2_csvSplit} from "../et2_core_common";
import type {IegwAppLocal} from "../../jsapi/egw_global";
import {egw} from "../../jsapi/egw_global";
import {ClassWithAttributes, ClassWithInterfaces} from "../et2_core_inheritance";
import {css, dedupeMixin, LitElement, PropertyValues, unsafeCSS} from "@lion/core";
import {css, LitElement, PropertyValues, unsafeCSS} from "lit";
import {dedupeMixin} from "@lion/core";
import type {et2_container} from "../et2_core_baseWidget";
import type {et2_DOMWidget} from "../et2_core_DOMWidget";

View File

@ -12,7 +12,7 @@
import {ExposeMixin, ExposeValue, MediaValue} from "./ExposeMixin";
import {Et2Description} from "../Et2Description/Et2Description";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {html} from "@lion/core";
import {html} from "lit";
/**
* Shows a description and if you click on it, it shows the file specified by href in gallery.

View File

@ -11,7 +11,7 @@
// Don't import this more than once
import "../../../../node_modules/blueimp-gallery/js/blueimp-gallery.min";
import {css, html, LitElement, render} from "@lion/core";
import {css, html, LitElement, render} from "lit";
import {et2_nextmatch} from "../et2_extension_nextmatch";
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
import {ET2_DATAVIEW_STEPSIZE} from "../et2_dataview_controller";

View File

@ -9,7 +9,8 @@
*/
import {classMap, css, html, LitElement} from "@lion/core";
import {css, html, LitElement} from "lit";
import {classMap} from "lit/directives/class-map.js";
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../../et2_core_interfaces";

View File

@ -8,7 +8,7 @@
*/
import {Et2Widget} from "../../Et2Widget/Et2Widget";
import {css} from "@lion/core";
import {css} from "lit";
import {SlDetails} from "@shoelace-style/shoelace";
import shoelace from "../../Styles/shoelace";

Some files were not shown because too many files have changed in this diff Show More