diff --git a/api/js/etemplate/Et2Select/FindSelectOptions.ts b/api/js/etemplate/Et2Select/FindSelectOptions.ts index cd9603ea7e..a835ffaf88 100644 --- a/api/js/etemplate/Et2Select/FindSelectOptions.ts +++ b/api/js/etemplate/Et2Select/FindSelectOptions.ts @@ -1,3 +1,5 @@ +import {SearchResult} from "../Et2Widget/SearchMixin"; + /** * Interface for select options * @@ -5,22 +7,8 @@ * For option groups, value is the list of sub-options. * */ -export interface SelectOption +export interface SelectOption extends SearchResult { - value : string | SelectOption[]; - label : string; - // Hover help text - title? : string; - // Related image or icon - icon? : string; - // Class applied to node - class? : string; - // 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; } /** diff --git a/api/js/etemplate/Et2Tree/Et2Tree.ts b/api/js/etemplate/Et2Tree/Et2Tree.ts index e06b3900cb..1e374d99d1 100644 --- a/api/js/etemplate/Et2Tree/Et2Tree.ts +++ b/api/js/etemplate/Et2Tree/Et2Tree.ts @@ -1,6 +1,6 @@ import {SlTreeItem} from "@shoelace-style/shoelace"; import {egw} from "../../jsapi/egw_global"; -import {find_select_options} from "../Et2Select/FindSelectOptions"; +import {find_select_options, SelectOption} from "../Et2Select/FindSelectOptions"; import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin"; import {css, html, LitElement, nothing, PropertyValues, TemplateResult} from "lit"; import {repeat} from "lit/directives/repeat.js"; @@ -14,7 +14,7 @@ import {EgwActionObject} from "../../egw_action/EgwActionObject"; import {EgwAction} from "../../egw_action/EgwAction"; import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree"; -export type TreeItemData = { +export type TreeItemData = SelectOption & { focused?: boolean; // Has children, but they may not be provided in item child: Boolean | 1, @@ -652,7 +652,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) /* if collapsed .. opended? leaf? */ - let img: String = selectOption.im0 ?? selectOption.im1 ?? selectOption.im2; + let img : String = selectOption.icon ?? selectOption.im0 ?? selectOption.im1 ?? selectOption.im2; if (img) { //sl-icon images need to be svgs if there is a png try to find the corresponding svg @@ -696,8 +696,8 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement) > - ${selectOption.text} - ${selectOption.item ? repeat(selectOption.item, this._optionTemplate) : nothing} + ${selectOption.label ?? selectOption.text} + ${selectOption.children ? repeat(selectOption.children, this._optionTemplate) : (selectOption.item ? repeat(selectOption.item, this._optionTemplate) : nothing)} ` } diff --git a/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts index 15966f101f..a2844c99d0 100644 --- a/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts +++ b/api/js/etemplate/Et2Tree/Et2TreeDropdown.ts @@ -1,6 +1,6 @@ import {LitElement, nothing} from "lit"; import {html, literal, StaticValue} from "lit/static-html.js"; -import {Et2Tree, TreeItemData} from "./Et2Tree"; +import {Et2Tree, TreeItemData, TreeSearchResult} from "./Et2Tree"; import {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin"; import {property} from "lit/decorators/property.js"; import {classMap} from "lit/directives/class-map.js"; @@ -15,7 +15,7 @@ import {SearchMixin, SearchResult, SearchResultsInterface} from "../Et2Widget/Se import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget"; -interface TreeSearchResult extends SearchResultsInterface +interface TreeSearchResults extends SearchResultsInterface { } @@ -43,7 +43,7 @@ type Constructor = new (...args : any[]) => T; * @csspart form-control - The form control that wraps the label, input, and help text. */ -export class Et2TreeDropdown extends SearchMixin & Et2InputWidgetInterface & typeof LitElement, SearchResult, TreeSearchResult>(Et2WidgetWithSelectMixin(LitElement)) +export class Et2TreeDropdown extends SearchMixin & Et2InputWidgetInterface & typeof LitElement, TreeSearchResult, TreeSearchResults>(Et2WidgetWithSelectMixin(LitElement)) { static get styles() @@ -89,6 +89,11 @@ export class Et2TreeDropdown extends SearchMixin & Et2InputWidg this.__value = []; } + updated() + { + // @ts-ignore Popup sometimes loses the anchor which breaks the sizing + this._popup.handleAnchorChange(); + } /** Selected tree leaves */ @property() set value(new_value : string | string[]) @@ -202,6 +207,22 @@ export class Et2TreeDropdown extends SearchMixin & Et2InputWidg this.treeOrSearch = "search"; } + + /** + * If you have a local list of options, you can search through them on the client and include them in the results. + * + * This is done independently from the server-side search, and the results are merged. + * + * @param {string} search + * @param {object} options + * @returns {Promise} + * @protected + */ + protected localSearch(search : string, searchOptions : object, localOptions : DataType[] = []) : Promise + { + return super.localSearch(search, searchOptions, this.select_options); + } + protected searchResultSelected() { super.searchResultSelected(); @@ -219,6 +240,9 @@ export class Et2TreeDropdown extends SearchMixin & Et2InputWidg // Done with search, show the tree this.treeOrSearch = "tree"; + // Close the dropdown + this.hide(); + this.requestUpdate("value"); } /** @@ -448,7 +472,7 @@ export class Et2TreeDropdown extends SearchMixin & Et2InputWidg return html`${map(value, (value, index) => { // Deal with value that is not in options - const option = this.optionSearch(value, this.select_options, 'value', 'item'); + const option = this.optionSearch(value, this.select_options, 'value', 'children'); return option ? this.tagTemplate(option) : nothing; })}`; } diff --git a/api/js/etemplate/Et2Widget/SearchMixin.ts b/api/js/etemplate/Et2Widget/SearchMixin.ts index b19d1e9e1d..a6c676c174 100644 --- a/api/js/etemplate/Et2Widget/SearchMixin.ts +++ b/api/js/etemplate/Et2Widget/SearchMixin.ts @@ -27,6 +27,11 @@ export type SearchResult = { // If a search is in progress, does this option match. // Automatically changed. isMatch? : boolean; + + // The item has children (option group) + hasChildren? : boolean, + // The item's children + children? : SearchResult[] } /** @@ -191,7 +196,7 @@ export const SearchMixin = } * @protected */ - protected localSearch(search : string, options : object) : Promise + protected localSearch(search : string, searchOptions : object, localOptions : DataType[] = []) : Promise { - const nullResults : Results = { + const local : Results = { results: [], total: 0 } - return Promise.resolve(this.processLocalResults(nullResults)); + let doSearch = function (options : DataType[], value : string) + { + options.forEach((option) => + { + if(typeof option !== "object") + { + return; + } + if(option.label?.includes(value) || option.value?.includes(value)) + { + local.results.push(option); + local.total++; + } + if(typeof option.children != "undefined" && Array.isArray(option.children)) + { + return doSearch(option.children, value); + } + }); + } + doSearch(localOptions, search); + + return Promise.resolve(this.processLocalResults(local)); } protected remoteSearch(search : string, options : object) : Promise @@ -375,7 +401,7 @@ export const SearchMixin = diff --git a/api/src/Etemplate/Widget/Tree.php b/api/src/Etemplate/Widget/Tree.php index 1df5766eb2..8a618e837e 100644 --- a/api/src/Etemplate/Widget/Tree.php +++ b/api/src/Etemplate/Widget/Tree.php @@ -523,9 +523,8 @@ class Tree extends Etemplate\Widget $cat_id_list[] = $cat['id']; if(!empty($cat['children'])) { - $category['item'] = []; - unset($cat['children']); - static::processCategory($cat['id'], $category['item'], $categories, $globals, $cat_id_list); + unset($category['children']); + static::processCategory($cat['id'], $category['children'], $categories, $globals, $cat_id_list); } $options[] = $category; }