mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 16:44:20 +01:00
Et2TreeDropdown improvements
- multiple=false rendering - fix some display search vs tree weirdness - fix some keyboard actions - translate placeholder - blur widget after option selected when multiple=false - implement clearable attribute
This commit is contained in:
parent
2e0010d138
commit
2da2ac81fa
@ -1043,7 +1043,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) implements Fin
|
|||||||
>
|
>
|
||||||
<sl-icon name="chevron-right" slot="expand-icon"></sl-icon>
|
<sl-icon name="chevron-right" slot="expand-icon"></sl-icon>
|
||||||
<sl-icon name="chevron-down" slot="collapse-icon"></sl-icon>
|
<sl-icon name="chevron-down" slot="collapse-icon"></sl-icon>
|
||||||
${repeat(this._selectOptions, this._optionTemplate)}
|
${repeat(this._selectOptions, (o) => o.value, this._optionTemplate)}
|
||||||
</sl-tree>
|
</sl-tree>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ export default css`
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-control-input {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
/* Label */
|
/* Label */
|
||||||
|
|
||||||
.form-control--has-label .form-control__label {
|
.form-control--has-label .form-control__label {
|
||||||
@ -37,14 +41,26 @@ export default css`
|
|||||||
margin-top: var(--sl-spacing-3x-small);
|
margin-top: var(--sl-spacing-3x-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-dropdown__value-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
.tree-dropdown__combobox {
|
.tree-dropdown__combobox {
|
||||||
min-height: calc(var(--sl-input-height-medium) - 2 * var(--sl-input-border-width));
|
min-height: calc(var(--sl-input-height-medium) - 2 * var(--sl-input-border-width));
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
background-color: var(--sl-input-background-color);
|
background-color: var(--sl-input-background-color);
|
||||||
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
|
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
|
||||||
@ -58,6 +74,12 @@ export default css`
|
|||||||
|
|
||||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow,
|
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow,
|
||||||
var(--sl-transition-fast) background-color;
|
var(--sl-transition-fast) background-color;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([multiple]) .tree-dropdown__combobox {
|
||||||
|
align-items: flex-start
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dropdown--disabled {
|
.tree-dropdown--disabled {
|
||||||
@ -91,6 +113,7 @@ export default css`
|
|||||||
transition: var(--sl-transition-medium) rotate ease;
|
transition: var(--sl-transition-medium) rotate ease;
|
||||||
rotate: 0;
|
rotate: 0;
|
||||||
margin-inline-start: var(--sl-spacing-small);
|
margin-inline-start: var(--sl-spacing-small);
|
||||||
|
order: 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-dropdown--open .tree-dropdown__expand-icon {
|
.tree-dropdown--open .tree-dropdown__expand-icon {
|
||||||
@ -103,9 +126,18 @@ export default css`
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Single */
|
||||||
|
|
||||||
|
|
||||||
|
/* End single */
|
||||||
|
|
||||||
/* Tags */
|
/* Tags */
|
||||||
|
|
||||||
.tree-dropdown__tags {
|
.tree-dropdown__tags {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-dropdown--multiple.tree-dropdown--has-value:not(.tree-dropdown--placeholder-visible) .tree-dropdown__tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 2 1 auto;
|
flex: 2 1 auto;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -116,6 +148,7 @@ export default css`
|
|||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Limit tag size */
|
/* Limit tag size */
|
||||||
|
|
||||||
.tree_tag {
|
.tree_tag {
|
||||||
@ -132,10 +165,6 @@ export default css`
|
|||||||
|
|
||||||
/* Search box */
|
/* Search box */
|
||||||
|
|
||||||
:host([readonly]) .tree-dropdown__search {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-dropdown__search {
|
.tree-dropdown__search {
|
||||||
flex: 1 1 7em;
|
flex: 1 1 7em;
|
||||||
order: 10;
|
order: 10;
|
||||||
@ -143,9 +172,10 @@ export default css`
|
|||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
color: var(--sl-input-color);
|
||||||
font-size: var(--sl-input-font-size-medium);
|
font-size: var(--sl-input-font-size-medium);
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
padding-inline: var(--sl-input-spacing-medium);
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control--medium .tree-dropdown__search {
|
.form-control--medium .tree-dropdown__search {
|
||||||
@ -153,6 +183,14 @@ export default css`
|
|||||||
height: calc(var(--sl-input-height-medium) * 0.8);
|
height: calc(var(--sl-input-height-medium) * 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([open]) .tree-dropdown__search {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:not([open])) .tree-dropdown--has-value.tree-dropdown--multiple .tree-dropdown__search {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-dropdown--disabled .tree-dropdown__search {
|
.tree-dropdown--disabled .tree-dropdown__search {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@ -161,17 +199,6 @@ export default css`
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tag takes full width when widget is not multiple and has value and does not have focus */
|
|
||||||
|
|
||||||
:host(:not([multiple])) .tree-dropdown--has-value .tree-dropdown__search {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host(:not([multiple])) .tree-dropdown--focused .tree-dropdown__search,
|
|
||||||
:host(:not([multiple])) .tree-dropdown--open .tree-dropdown__search {
|
|
||||||
display: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-dropdown__suffix {
|
.tree-dropdown__suffix {
|
||||||
order: 20;
|
order: 20;
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,10 @@ import {SlPopup, SlRemoveEvent, SlTreeItem} from "@shoelace-style/shoelace";
|
|||||||
import shoelace from "../Styles/shoelace";
|
import shoelace from "../Styles/shoelace";
|
||||||
import styles from "./Et2TreeDropdown.styles";
|
import styles from "./Et2TreeDropdown.styles";
|
||||||
import {Et2Tag} from "../Et2Select/Tag/Et2Tag";
|
import {Et2Tag} from "../Et2Select/Tag/Et2Tag";
|
||||||
import {SearchMixin, SearchResult, SearchResultsInterface} from "../Et2Widget/SearchMixin";
|
import {SearchMixin, SearchResult, SearchResultElement, SearchResultsInterface} from "../Et2Widget/SearchMixin";
|
||||||
import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||||
import {Required} from "../Validators/Required";
|
import {Required} from "../Validators/Required";
|
||||||
|
import {SelectOption} from "../Et2Select/FindSelectOptions";
|
||||||
|
|
||||||
|
|
||||||
interface TreeSearchResults extends SearchResultsInterface<TreeItemData>
|
interface TreeSearchResults extends SearchResultsInterface<TreeItemData>
|
||||||
@ -56,11 +57,26 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of properties that get translated
|
||||||
|
* @returns object
|
||||||
|
*/
|
||||||
|
static get translate()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
...super.translate,
|
||||||
|
placeholder: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Placeholder text to show as a hint when the select is empty. */
|
/** Placeholder text to show as a hint when the select is empty. */
|
||||||
@property() placeholder = "";
|
@property() placeholder = "";
|
||||||
|
|
||||||
@property({type: Boolean, reflect: true}) multiple = false;
|
@property({type: Boolean, reflect: true}) multiple = false;
|
||||||
|
|
||||||
|
/** Adds a clear button when the dropdown is not empty. */
|
||||||
|
@property({type: Boolean}) clearable = false;
|
||||||
|
|
||||||
/** The component's help text. If you need to display HTML, use the `help-text` slot instead. */
|
/** The component's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||||
@property({attribute: 'help-text'}) helpText = "";
|
@property({attribute: 'help-text'}) helpText = "";
|
||||||
|
|
||||||
@ -122,6 +138,8 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
protected readonly hasSlotController = new HasSlotController(<LitElement><unknown>this, "help-text", "label");
|
protected readonly hasSlotController = new HasSlotController(<LitElement><unknown>this, "help-text", "label");
|
||||||
private __value : string[];
|
private __value : string[];
|
||||||
|
|
||||||
|
protected displayLabel = '';
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
@ -133,11 +151,30 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
connectedCallback()
|
connectedCallback()
|
||||||
{
|
{
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
document.addEventListener("click", this.handleDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback()
|
disconnectedCallback()
|
||||||
{
|
{
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
document.removeEventListener("click", this.handleDocumentClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProperties)
|
||||||
|
{
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
// Child tree not updating when our emptyLabel changes
|
||||||
|
if(this._tree && (changedProperties.has("select_options") || changedProperties.has("emptyLabel")))
|
||||||
|
{
|
||||||
|
let options = this.multiple || !this.emptyLabel ? this.select_options : [{
|
||||||
|
value: "",
|
||||||
|
label: this.emptyLabel
|
||||||
|
}, ...this.select_options];
|
||||||
|
|
||||||
|
this._tree._selectOptions = <TreeItemData[]>options;
|
||||||
|
this._tree.requestUpdate("_selectOptions");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties : PropertyValues)
|
updated(changedProperties : PropertyValues)
|
||||||
@ -188,6 +225,17 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
const oldValue = this.__value;
|
const oldValue = this.__value;
|
||||||
// Filter to make sure there are no trailing commas or duplicates
|
// Filter to make sure there are no trailing commas or duplicates
|
||||||
this.__value = Array.from(new Set(<string[]>new_value.filter(v => v)));
|
this.__value = Array.from(new Set(<string[]>new_value.filter(v => v)));
|
||||||
|
|
||||||
|
this.displayLabel = "";
|
||||||
|
if(!this.multiple)
|
||||||
|
{
|
||||||
|
const option = this.optionSearch(this.__value[0], this.select_options, 'value', 'children');
|
||||||
|
if(option)
|
||||||
|
{
|
||||||
|
this.displayLabel = option.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.requestUpdate("value", oldValue);
|
this.requestUpdate("value", oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,47 +247,49 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get select_options() : SelectOption[]
|
||||||
|
{
|
||||||
|
return super.select_options;
|
||||||
|
}
|
||||||
|
|
||||||
|
set select_options(new_options : SelectOption[])
|
||||||
|
{
|
||||||
|
super.select_options = new_options;
|
||||||
|
|
||||||
|
// Overridden so we can update displayLabel in the case where value got set before selectOptions
|
||||||
|
if(this.value && !this.multiple)
|
||||||
|
{
|
||||||
|
const option = this.optionSearch(typeof this.value == "string" ? this.value : this.value[0], this.select_options, 'value', 'children');
|
||||||
|
if(option)
|
||||||
|
{
|
||||||
|
this.displayLabel = option.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets focus on the control. */
|
/** Sets focus on the control. */
|
||||||
focus(options? : FocusOptions)
|
focus(options? : FocusOptions)
|
||||||
{
|
{
|
||||||
this.hasFocus = true;
|
this.handleFocus();
|
||||||
// Should not be needed, but not firing the update
|
|
||||||
this.requestUpdate("hasFocus");
|
|
||||||
|
|
||||||
if(this._searchNode)
|
|
||||||
{
|
|
||||||
this._searchNode.focus(options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removes focus from the control. */
|
/** Removes focus from the control. */
|
||||||
blur()
|
blur()
|
||||||
{
|
{
|
||||||
this.open = false;
|
this.handleBlur();
|
||||||
this.treeOrSearch = "tree";
|
|
||||||
this.hasFocus = false;
|
|
||||||
this._popup.active = false;
|
|
||||||
// Should not be needed, but not firing the update
|
|
||||||
this.requestUpdate("open");
|
|
||||||
this.requestUpdate("hasFocus");
|
|
||||||
this._searchNode.blur();
|
|
||||||
|
|
||||||
clearTimeout(this._searchTimeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Shows the tree. */
|
/** Shows the tree. */
|
||||||
async show()
|
async show()
|
||||||
{
|
{
|
||||||
if(this.open || this.disabled)
|
if(this.readonly || this.disabled)
|
||||||
{
|
{
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.requestUpdate("open", true);
|
this.requestUpdate("open", true);
|
||||||
return undefined;
|
return this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", this.handleDocumentClick);
|
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.requestUpdate("open", false)
|
this.requestUpdate("open", false)
|
||||||
return this.updateComplete
|
return this.updateComplete
|
||||||
@ -253,8 +303,6 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener("click", this.handleDocumentClick);
|
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this._popup.active = false;
|
this._popup.active = false;
|
||||||
this._searchNode.value = "";
|
this._searchNode.value = "";
|
||||||
@ -311,10 +359,25 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
return super.localSearch(search, searchOptions, this.select_options);
|
return super.localSearch(search, searchOptions, this.select_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a search result's selected state
|
||||||
|
* Overridden to handle multiple attribute so only 1 result is selected
|
||||||
|
*/
|
||||||
|
protected toggleResultSelection(result : HTMLElement & SearchResultElement, force? : boolean)
|
||||||
|
{
|
||||||
|
if(!this.multiple)
|
||||||
|
{
|
||||||
|
this._resultNodes.forEach(t => t.selected = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.toggleResultSelection(result, force);
|
||||||
|
}
|
||||||
|
|
||||||
protected searchResultSelected()
|
protected searchResultSelected()
|
||||||
{
|
{
|
||||||
super.searchResultSelected();
|
super.searchResultSelected();
|
||||||
|
|
||||||
|
const oldValue = [...this.value];
|
||||||
if(this.multiple && typeof this.value !== "undefined")
|
if(this.multiple && typeof this.value !== "undefined")
|
||||||
{
|
{
|
||||||
// Add in the new result(s), no duplicates
|
// Add in the new result(s), no duplicates
|
||||||
@ -328,9 +391,50 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
|
|
||||||
// Done with search, show the tree
|
// Done with search, show the tree
|
||||||
this.treeOrSearch = "tree";
|
this.treeOrSearch = "tree";
|
||||||
// Close the dropdown
|
|
||||||
this.hide();
|
// Close the dropdown, move on
|
||||||
this.requestUpdate("value");
|
if(!this.multiple || this.egw().preference("select_multiple_close") == "close")
|
||||||
|
{
|
||||||
|
this.blur();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._tree.value = <string[]>this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update values
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||||
|
});
|
||||||
|
this._tree.requestUpdate("value", oldValue);
|
||||||
|
this.requestUpdate("value", oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClearClick(event : MouseEvent)
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if(this.value.length > 0)
|
||||||
|
{
|
||||||
|
this.value = [];
|
||||||
|
this.displayInput.focus({preventScroll: true});
|
||||||
|
|
||||||
|
// Emit after update
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.emit('sl-clear');
|
||||||
|
this.emit('sl-input');
|
||||||
|
this.emit('sl-change');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClearMouseDown(event : MouseEvent)
|
||||||
|
{
|
||||||
|
// Don't lose focus or propagate events when clicking the clear button
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -403,11 +507,60 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.hide()
|
this.hide()
|
||||||
}
|
}
|
||||||
else
|
this.blur();
|
||||||
{
|
}
|
||||||
this.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private handleFocus()
|
||||||
|
{
|
||||||
|
this.hasFocus = true;
|
||||||
|
// Should not be needed, but not firing the update
|
||||||
|
this.requestUpdate("hasFocus");
|
||||||
|
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
if(this._searchNode)
|
||||||
|
{
|
||||||
|
this._searchNode.focus();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._tree.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("sl-focus"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBlur()
|
||||||
|
{
|
||||||
|
this.open = false;
|
||||||
|
this.treeOrSearch = "tree";
|
||||||
|
this.hasFocus = false;
|
||||||
|
this.resultsOpen = false;
|
||||||
|
this._popup.active = false;
|
||||||
|
// Should not be needed, but not firing the update
|
||||||
|
this.requestUpdate("resultsOpen");
|
||||||
|
this.requestUpdate("open");
|
||||||
|
this.requestUpdate("hasFocus");
|
||||||
|
this._searchNode?.blur();
|
||||||
|
|
||||||
|
clearTimeout(this._searchTimeout);
|
||||||
|
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event("sl-blur"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleClick(event)
|
||||||
|
{
|
||||||
|
// Open if clicking somewhere in the widget
|
||||||
|
if(event.target.classList.contains("tree-dropdown__combobox"))
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
this.show();
|
||||||
|
this.handleFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSearchFocus()
|
private handleSearchFocus()
|
||||||
@ -419,12 +572,28 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
// Reset tags to not take focus
|
// Reset tags to not take focus
|
||||||
this.setCurrentTag(null);
|
this.setCurrentTag(null);
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearchBlur(event)
|
||||||
|
{
|
||||||
|
// Focus lost to some other internal component - ignore it
|
||||||
|
if(event.composedPath().includes(this.shadowRoot))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleBlur();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchKeyDown(event)
|
handleSearchKeyDown(event)
|
||||||
{
|
{
|
||||||
super.handleSearchKeyDown(event);
|
super.handleSearchKeyDown(event);
|
||||||
|
|
||||||
|
if(event.key == "ArrowDown" && !this.open && !this.resultsOpen)
|
||||||
|
{
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Left at beginning goes to tags
|
// Left at beginning goes to tags
|
||||||
if(this._searchNode.selectionStart == 0 && event.key == "ArrowLeft")
|
if(this._searchNode.selectionStart == 0 && event.key == "ArrowLeft")
|
||||||
{
|
{
|
||||||
@ -492,30 +661,31 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
this.value = [...this.value, id];
|
this.value = [...this.value, id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.requestUpdate("value", oldValue);
|
||||||
|
|
||||||
this.updateComplete.then(() =>
|
this.updateComplete.then(() =>
|
||||||
{
|
{
|
||||||
this.dispatchEvent(new Event("change", {bubbles: true}));
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||||
});
|
});
|
||||||
if(!this.multiple)
|
if(!this.multiple || this.egw().preference("select_multiple_close") == "close")
|
||||||
{
|
{
|
||||||
this.hide();
|
this.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTriggerClick()
|
handleTriggerClick(event)
|
||||||
{
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
this.hasFocus = true;
|
this.hasFocus = true;
|
||||||
if(this.open)
|
if(this.open)
|
||||||
{
|
{
|
||||||
this._popup.active = false;
|
this._popup.active = false;
|
||||||
this._searchNode.value = "";
|
this._searchNode.value = "";
|
||||||
document.removeEventListener("click", this.handleDocumentClick);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this._popup.active = true;
|
this._popup.active = true;
|
||||||
document.addEventListener("click", this.handleDocumentClick);
|
|
||||||
}
|
}
|
||||||
this.open = this._popup.active;
|
this.open = this._popup.active;
|
||||||
this.treeOrSearch = "tree";
|
this.treeOrSearch = "tree";
|
||||||
@ -523,7 +693,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
this.updateComplete.then(() =>
|
this.updateComplete.then(() =>
|
||||||
{
|
{
|
||||||
this._tree.style.minWidth = getComputedStyle(this).width;
|
this._tree.style.minWidth = getComputedStyle(this).width;
|
||||||
this._tree.focus();
|
this.focus();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,18 +716,49 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
|
|
||||||
inputTemplate()
|
inputTemplate()
|
||||||
{
|
{
|
||||||
|
let placeholder = this.egw().lang("search");
|
||||||
|
if(this.disabled || this.readonly || (this.open && this.value))
|
||||||
|
{
|
||||||
|
placeholder = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
placeholder = this.emptyLabel || this.placeholder;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<input id="search" type="text" part="input"
|
<input id="search" type="text" part="input"
|
||||||
class="tree-dropdown__search search__input"
|
class="tree-dropdown__search search__input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocapitalize="off"
|
||||||
|
aria-controls="listbox"
|
||||||
|
aria-expanded=${this.open ? 'true' : 'false'}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-labelledby="label"
|
||||||
|
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||||
|
aria-describedby="help-text"
|
||||||
|
role="combobox"
|
||||||
|
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
?readonly=${this.readonly}
|
?readonly=${this.readonly}
|
||||||
placeholder="${this.hasFocus || this.value.length > 0 || this.disabled || this.readonly ? "" : this.egw().lang(this.placeholder || this.emptyLabel)}"
|
placeholder="${placeholder}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
.value=${this.hasFocus ? "" : this.displayLabel}
|
||||||
@keydown=${this.handleSearchKeyDown}
|
@keydown=${this.handleSearchKeyDown}
|
||||||
@blur=${() => {this.hasFocus = false;}}
|
@blur=${this.handleSearchBlur}
|
||||||
@focus=${this.handleSearchFocus}
|
@focus=${this.handleSearchFocus}
|
||||||
@paste=${this.handlePaste}
|
@paste=${this.handlePaste}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="tree-dropdown__value-input"
|
||||||
|
type="text"
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
?required=${this.required}
|
||||||
|
.value=${Array.isArray(this.value) ? this.value.join(', ') : this.value}
|
||||||
|
tabindex="-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
@focus=${this.handleFocus}
|
||||||
|
@blur=${this.handleBlur}
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -578,15 +779,23 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
return literal`et2-tag`;
|
return literal`et2-tag`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the currently selected values as tags when multiple=true
|
||||||
|
*
|
||||||
|
* @returns {TemplateResult}
|
||||||
|
*/
|
||||||
tagsTemplate()
|
tagsTemplate()
|
||||||
{
|
{
|
||||||
const value = this.getValueAsArray();
|
const value = this.getValueAsArray();
|
||||||
return html`${map(value, (value, index) =>
|
return html`
|
||||||
{
|
<div part="tags" class="tree-dropdown__tags">
|
||||||
// Deal with value that is not in options
|
${map(value, (value, index) =>
|
||||||
const option = this.optionSearch(value, this.select_options, 'value', 'children');
|
{
|
||||||
return option ? this.tagTemplate(option) : nothing;
|
// Deal with value that is not in options
|
||||||
})}`;
|
const option = this.optionSearch(value, this.select_options, 'value', 'children');
|
||||||
|
return option ? this.tagTemplate(option) : nothing;
|
||||||
|
})}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
tagTemplate(option : TreeItemData)
|
tagTemplate(option : TreeItemData)
|
||||||
@ -633,8 +842,12 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
const hasLabel = this.label ? true : !!hasLabelSlot;
|
const hasLabel = this.label ? true : !!hasLabelSlot;
|
||||||
const hasValue = this.value && this.value.length > 0;
|
const hasValue = this.value && this.value.length > 0;
|
||||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||||
|
const hasClearIcon = this.clearable && !this.disabled && this.value.length > 0;
|
||||||
const isPlaceholderVisible = (this.placeholder || this.emptyLabel) && this.value.length === 0 && !this.disabled && !this.readonly;
|
const isPlaceholderVisible = (this.placeholder || this.emptyLabel) && this.value.length === 0 && !this.disabled && !this.readonly;
|
||||||
|
let options = this.multiple || !this.emptyLabel ? this.select_options : [{
|
||||||
|
value: "",
|
||||||
|
label: this.emptyLabel
|
||||||
|
}, ...this.select_options];
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
part="form-control"
|
part="form-control"
|
||||||
@ -662,6 +875,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
'tree-dropdown--disabled': this.disabled,
|
'tree-dropdown--disabled': this.disabled,
|
||||||
'tree-dropdown--readonly': this.readonly,
|
'tree-dropdown--readonly': this.readonly,
|
||||||
'tree-dropdown--focused': this.hasFocus,
|
'tree-dropdown--focused': this.hasFocus,
|
||||||
|
'tree-dropdown--multiple': this.multiple,
|
||||||
'tree-dropdown--placeholder-visible': isPlaceholderVisible,
|
'tree-dropdown--placeholder-visible': isPlaceholderVisible,
|
||||||
'tree-dropdown--searching': this.treeOrSearch == "search",
|
'tree-dropdown--searching': this.treeOrSearch == "search",
|
||||||
'tree-dropdown--has-value': hasValue
|
'tree-dropdown--has-value': hasValue
|
||||||
@ -672,7 +886,6 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
auto-size-padding="10"
|
auto-size-padding="10"
|
||||||
?active=${this.open}
|
?active=${this.open}
|
||||||
placement=${this.placement || "bottom"}
|
placement=${this.placement || "bottom"}
|
||||||
stay-open-on-select
|
|
||||||
strategy="fixed"
|
strategy="fixed"
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
>
|
>
|
||||||
@ -681,12 +894,28 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
class="tree-dropdown__combobox"
|
class="tree-dropdown__combobox"
|
||||||
slot="anchor"
|
slot="anchor"
|
||||||
@keydown=${this.handleComboboxKeyDown}
|
@keydown=${this.handleComboboxKeyDown}
|
||||||
|
@click=${this.handleClick}
|
||||||
>
|
>
|
||||||
<slot part="prefix" name="prefix" class="tree-dropdown__prefix"></slot>
|
<slot part="prefix" name="prefix" class="tree-dropdown__prefix"></slot>
|
||||||
<div part="tags" class="tree-dropdown__tags">
|
${this.multiple ? this.tagsTemplate() : nothing}
|
||||||
${this.tagsTemplate()}
|
${this.inputTemplate()}
|
||||||
${this.inputTemplate()}
|
${hasClearIcon
|
||||||
</div>
|
? html`
|
||||||
|
<button
|
||||||
|
part="clear-button"
|
||||||
|
class="select__clear"
|
||||||
|
type="button"
|
||||||
|
aria-label=${this.localize.term('clearEntry')}
|
||||||
|
@mousedown=${this.handleClearMouseDown}
|
||||||
|
@click=${this.handleClearClick}
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<slot name="clear-icon">
|
||||||
|
<sl-icon name="x-circle-fill" library="system"></sl-icon>
|
||||||
|
</slot>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
<slot part="suffix" name="suffix" class="tree-dropdown__suffix"></slot>
|
<slot part="suffix" name="suffix" class="tree-dropdown__suffix"></slot>
|
||||||
<slot name="expand-icon" part="expand-icon" class="tree-dropdown__expand-icon"
|
<slot name="expand-icon" part="expand-icon" class="tree-dropdown__expand-icon"
|
||||||
@click=${this.handleTriggerClick}>
|
@click=${this.handleTriggerClick}>
|
||||||
@ -702,7 +931,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
?readonly=${this.readonly}
|
?readonly=${this.readonly}
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
value=${this.multiple ? nothing : this.value}
|
value=${this.multiple ? nothing : this.value}
|
||||||
._selectOptions=${this.select_options}
|
._selectOptions=${options}
|
||||||
.actions=${this.actions}
|
.actions=${this.actions}
|
||||||
.styleTemplate=${() => this.styleTemplate()}
|
.styleTemplate=${() => this.styleTemplate()}
|
||||||
.autoloading="${this.autoloading}"
|
.autoloading="${this.autoloading}"
|
||||||
|
@ -505,6 +505,11 @@ export const SearchMixin = <T extends Constructor<Et2InputWidgetInterface &
|
|||||||
{
|
{
|
||||||
this.value = [this.selectedResults[0]?.value] ?? [];
|
this.value = [this.selectedResults[0]?.value] ?? [];
|
||||||
}
|
}
|
||||||
|
// Dispatch the change event
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||||
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this.updateComplete.then(() =>
|
this.updateComplete.then(() =>
|
||||||
|
@ -3797,6 +3797,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext
|
|||||||
!select.select_options.filter(option => option.value === '').length)
|
!select.select_options.filter(option => option.value === '').length)
|
||||||
{
|
{
|
||||||
select.emptyLabel = this.egw().lang('All categories');
|
select.emptyLabel = this.egw().lang('All categories');
|
||||||
|
// requestUpdate because widget is not firing update itself
|
||||||
|
select.requestUpdate("emptyLabel");
|
||||||
}
|
}
|
||||||
select.requestUpdate("value");
|
select.requestUpdate("value");
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user