mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 10:53:39 +01:00
parent
0f77eca5c4
commit
e323cd1d79
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cropper styles constant
|
||||
*/
|
||||
import {css} from "@lion/core";
|
||||
import {css} from "lit";
|
||||
|
||||
/*!
|
||||
* Cropper.js v1.5.12
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -8,7 +8,7 @@
|
||||
* @author Nathan Gray
|
||||
*/
|
||||
|
||||
import {css, html, LitElement} from "@lion/core";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import {ButtonMixin} from "./ButtonMixin";
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, html} from "@lion/core";
|
||||
import {css, html} from "lit";
|
||||
import {Et2DateDuration, formatOptions} from "./Et2DateDuration";
|
||||
import {dateStyles} from "./DateStyles";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {html} from "@lion/core";
|
||||
import {html} from "lit";
|
||||
import {parseDate, parseDateTime} from "./Et2Date";
|
||||
import {Et2DateReadonly} from "./Et2DateReadonly";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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
@ -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>
|
||||
`;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
1181
api/js/etemplate/Et2Select/SearchMixin.js
Normal file
1181
api/js/etemplate/Et2Select/SearchMixin.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
});
|
@ -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[])
|
17
api/js/etemplate/Et2Select/Select/Et2SelectApp.ts
Normal file
17
api/js/etemplate/Et2Select/Select/Et2SelectApp.ts
Normal 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);
|
26
api/js/etemplate/Et2Select/Select/Et2SelectBitwise.ts
Normal file
26
api/js/etemplate/Et2Select/Select/Et2SelectBitwise.ts
Normal 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);
|
28
api/js/etemplate/Et2Select/Select/Et2SelectBool.ts
Normal file
28
api/js/etemplate/Et2Select/Select/Et2SelectBool.ts
Normal 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);
|
@ -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`;
|
||||
}
|
||||
|
||||
/**
|
@ -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");
|
||||
});
|
||||
}
|
14
api/js/etemplate/Et2Select/Select/Et2SelectDay.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectDay.ts
Normal 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);
|
53
api/js/etemplate/Et2Select/Select/Et2SelectDayOfWeek.ts
Normal file
53
api/js/etemplate/Et2Select/Select/Et2SelectDayOfWeek.ts
Normal 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);
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
14
api/js/etemplate/Et2Select/Select/Et2SelectHour.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectHour.ts
Normal 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);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectLang.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectLang.ts
Normal 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);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectMonth.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectMonth.ts
Normal 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);
|
52
api/js/etemplate/Et2Select/Select/Et2SelectNumber.ts
Normal file
52
api/js/etemplate/Et2Select/Select/Et2SelectNumber.ts
Normal 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);
|
15
api/js/etemplate/Et2Select/Select/Et2SelectPercent.ts
Normal file
15
api/js/etemplate/Et2Select/Select/Et2SelectPercent.ts
Normal 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);
|
14
api/js/etemplate/Et2Select/Select/Et2SelectPriority.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectPriority.ts
Normal 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);
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
45
api/js/etemplate/Et2Select/Select/Et2SelectState.ts
Normal file
45
api/js/etemplate/Et2Select/Select/Et2SelectState.ts
Normal 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);
|
61
api/js/etemplate/Et2Select/Select/Et2SelectTab.ts
Normal file
61
api/js/etemplate/Et2Select/Select/Et2SelectTab.ts
Normal 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);
|
@ -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
|
||||
{
|
14
api/js/etemplate/Et2Select/Select/Et2SelectTimezone.ts
Normal file
14
api/js/etemplate/Et2Select/Select/Et2SelectTimezone.ts
Normal 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);
|
25
api/js/etemplate/Et2Select/Select/Et2SelectYear.ts
Normal file
25
api/js/etemplate/Et2Select/Select/Et2SelectYear.ts
Normal 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);
|
@ -1,5 +1,5 @@
|
||||
import {SelectOption} from "./FindSelectOptions";
|
||||
import {LitElement} from "@lion/core";
|
||||
import {LitElement} from "lit";
|
||||
|
||||
/**
|
||||
* EGroupware eTemplate2 - SelectAccountMixin
|
||||
|
25
api/js/etemplate/Et2Select/SelectTypes.ts
Normal file
25
api/js/etemplate/Et2Select/SelectTypes.ts
Normal 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';
|
285
api/js/etemplate/Et2Select/StaticOptions.js
Normal file
285
api/js/etemplate/Et2Select/StaticOptions.js
Normal 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
|
@ -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[]
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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>`;
|
||||
|
@ -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>
|
||||
`
|
||||
: ''}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
@ -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 > *');
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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'));
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user