mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-05 05:29:13 +01:00
Implement local search in SearchMixin & for Et2TreeDropdown
Add ability for SearchResult to have children
This commit is contained in:
parent
5e32896ccd
commit
a9a26ffe39
@ -1,3 +1,5 @@
|
|||||||
|
import {SearchResult} from "../Et2Widget/SearchMixin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for select options
|
* Interface for select options
|
||||||
*
|
*
|
||||||
@ -5,22 +7,8 @@
|
|||||||
* For option groups, value is the list of sub-options.
|
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {SlTreeItem} from "@shoelace-style/shoelace";
|
import {SlTreeItem} from "@shoelace-style/shoelace";
|
||||||
import {egw} from "../../jsapi/egw_global";
|
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 {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
|
||||||
import {css, html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
import {css, html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
||||||
import {repeat} from "lit/directives/repeat.js";
|
import {repeat} from "lit/directives/repeat.js";
|
||||||
@ -14,7 +14,7 @@ import {EgwActionObject} from "../../egw_action/EgwActionObject";
|
|||||||
import {EgwAction} from "../../egw_action/EgwAction";
|
import {EgwAction} from "../../egw_action/EgwAction";
|
||||||
import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree";
|
import {EgwDragDropShoelaceTree} from "../../egw_action/EgwDragDropShoelaceTree";
|
||||||
|
|
||||||
export type TreeItemData = {
|
export type TreeItemData = SelectOption & {
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
// Has children, but they may not be provided in item
|
// Has children, but they may not be provided in item
|
||||||
child: Boolean | 1,
|
child: Boolean | 1,
|
||||||
@ -652,7 +652,7 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
|||||||
/*
|
/*
|
||||||
if collapsed .. opended? leaf?
|
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)
|
if (img)
|
||||||
{
|
{
|
||||||
//sl-icon images need to be svgs if there is a png try to find the corresponding svg
|
//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)
|
|||||||
>
|
>
|
||||||
<sl-icon src="${img ?? nothing}"></sl-icon>
|
<sl-icon src="${img ?? nothing}"></sl-icon>
|
||||||
|
|
||||||
<span class="tree-item__label">${selectOption.text}</span>
|
<span class="tree-item__label">${selectOption.label ?? selectOption.text}</span>
|
||||||
${selectOption.item ? repeat(selectOption.item, this._optionTemplate) : nothing}
|
${selectOption.children ? repeat(selectOption.children, this._optionTemplate) : (selectOption.item ? repeat(selectOption.item, this._optionTemplate) : nothing)}
|
||||||
</sl-tree-item>`
|
</sl-tree-item>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {LitElement, nothing} from "lit";
|
import {LitElement, nothing} from "lit";
|
||||||
import {html, literal, StaticValue} from "lit/static-html.js";
|
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 {Et2WidgetWithSelectMixin} from "../Et2Select/Et2WidgetWithSelectMixin";
|
||||||
import {property} from "lit/decorators/property.js";
|
import {property} from "lit/decorators/property.js";
|
||||||
import {classMap} from "lit/directives/class-map.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";
|
import {Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
|
||||||
|
|
||||||
|
|
||||||
interface TreeSearchResult extends SearchResultsInterface<SearchResult>
|
interface TreeSearchResults extends SearchResultsInterface<TreeSearchResult>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ type Constructor<T = {}> = new (...args : any[]) => T;
|
|||||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidgetInterface & typeof LitElement, SearchResult, TreeSearchResult>(Et2WidgetWithSelectMixin(LitElement))
|
export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidgetInterface & typeof LitElement, TreeSearchResult, TreeSearchResults>(Et2WidgetWithSelectMixin(LitElement))
|
||||||
{
|
{
|
||||||
|
|
||||||
static get styles()
|
static get styles()
|
||||||
@ -89,6 +89,11 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
this.__value = [];
|
this.__value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated()
|
||||||
|
{
|
||||||
|
// @ts-ignore Popup sometimes loses the anchor which breaks the sizing
|
||||||
|
this._popup.handleAnchorChange();
|
||||||
|
}
|
||||||
/** Selected tree leaves */
|
/** Selected tree leaves */
|
||||||
@property()
|
@property()
|
||||||
set value(new_value : string | string[])
|
set value(new_value : string | string[])
|
||||||
@ -202,6 +207,22 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
this.treeOrSearch = "search";
|
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<any[]>}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected localSearch<DataType extends SearchResult>(search : string, searchOptions : object, localOptions : DataType[] = []) : Promise<DataType[]>
|
||||||
|
{
|
||||||
|
return super.localSearch(search, searchOptions, this.select_options);
|
||||||
|
}
|
||||||
|
|
||||||
protected searchResultSelected()
|
protected searchResultSelected()
|
||||||
{
|
{
|
||||||
super.searchResultSelected();
|
super.searchResultSelected();
|
||||||
@ -219,6 +240,9 @@ 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();
|
||||||
|
this.requestUpdate("value");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -448,7 +472,7 @@ export class Et2TreeDropdown extends SearchMixin<Constructor<any> & Et2InputWidg
|
|||||||
return html`${map(value, (value, index) =>
|
return html`${map(value, (value, index) =>
|
||||||
{
|
{
|
||||||
// Deal with value that is not in options
|
// 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;
|
return option ? this.tagTemplate(option) : nothing;
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,11 @@ export type SearchResult = {
|
|||||||
// If a search is in progress, does this option match.
|
// If a search is in progress, does this option match.
|
||||||
// Automatically changed.
|
// Automatically changed.
|
||||||
isMatch? : boolean;
|
isMatch? : boolean;
|
||||||
|
|
||||||
|
// The item has children (option group)
|
||||||
|
hasChildren? : boolean,
|
||||||
|
// The item's children
|
||||||
|
children? : SearchResult[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,7 +196,7 @@ export const SearchMixin = <T extends Constructor<Et2InputWidgetInterface &
|
|||||||
// The component has the focus
|
// The component has the focus
|
||||||
@state() hasFocus = false;
|
@state() hasFocus = false;
|
||||||
// For keyboard navigation of search results
|
// For keyboard navigation of search results
|
||||||
@state() currentResult : HTMLElement & SearchResultElement = null;
|
@state() currentResult : LitElement & SearchResultElement = null;
|
||||||
// Search result nodes marked as "selected"
|
// Search result nodes marked as "selected"
|
||||||
@state() selectedResults : (HTMLElement & SearchResultElement)[] = [];
|
@state() selectedResults : (HTMLElement & SearchResultElement)[] = [];
|
||||||
|
|
||||||
@ -209,7 +214,7 @@ export const SearchMixin = <T extends Constructor<Et2InputWidgetInterface &
|
|||||||
// Element where we render the search results
|
// Element where we render the search results
|
||||||
protected get _listNode() : HTMLElement { return this.shadowRoot.querySelector("#listbox");}
|
protected get _listNode() : HTMLElement { return this.shadowRoot.querySelector("#listbox");}
|
||||||
|
|
||||||
protected get _resultNodes() : (HTMLElement & SearchResultElement)[] { return this._listNode ? Array.from(this._listNode.querySelectorAll("*:not(div)")) : [];}
|
protected get _resultNodes() : (LitElement & SearchResultElement)[] { return this._listNode ? Array.from(this._listNode.querySelectorAll("*:not(div)")) : [];}
|
||||||
|
|
||||||
constructor(...args : any[])
|
constructor(...args : any[])
|
||||||
{
|
{
|
||||||
@ -278,17 +283,38 @@ export const SearchMixin = <T extends Constructor<Et2InputWidgetInterface &
|
|||||||
* This is done independently from the server-side search, and the results are merged.
|
* This is done independently from the server-side search, and the results are merged.
|
||||||
*
|
*
|
||||||
* @param {string} search
|
* @param {string} search
|
||||||
* @param {object} options
|
* @param {object} searchOptions
|
||||||
* @returns {Promise<any[]>}
|
* @returns {Promise<any[]>}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected localSearch<DataType>(search : string, options : object) : Promise<DataType[]>
|
protected localSearch<DataType extends SearchResult>(search : string, searchOptions : object, localOptions : DataType[] = []) : Promise<DataType[]>
|
||||||
{
|
{
|
||||||
const nullResults : Results = <Results><unknown>{
|
const local : Results = <Results><unknown>{
|
||||||
results: <DataType[]>[],
|
results: <DataType[]>[],
|
||||||
total: 0
|
total: 0
|
||||||
}
|
}
|
||||||
return Promise.resolve(this.processLocalResults(nullResults));
|
let doSearch = function <DataType extends SearchResult>(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<DataType>(search : string, options : object) : Promise<DataType[]>
|
protected remoteSearch<DataType>(search : string, options : object) : Promise<DataType[]>
|
||||||
@ -375,7 +401,7 @@ export const SearchMixin = <T extends Constructor<Et2InputWidgetInterface &
|
|||||||
* Sets the current search result, which is the one the user is currently interacting with (e.g. via keyboard).
|
* Sets the current search result, which is the one the user is currently interacting with (e.g. via keyboard).
|
||||||
* Only one result may be "current" at a time.
|
* Only one result may be "current" at a time.
|
||||||
*/
|
*/
|
||||||
private setCurrentResult(result : HTMLElement & SearchResultElement | null)
|
private setCurrentResult(result : LitElement & SearchResultElement | null)
|
||||||
{
|
{
|
||||||
// Clear selection
|
// Clear selection
|
||||||
this._resultNodes.forEach((el) =>
|
this._resultNodes.forEach((el) =>
|
||||||
|
@ -523,9 +523,8 @@ class Tree extends Etemplate\Widget
|
|||||||
$cat_id_list[] = $cat['id'];
|
$cat_id_list[] = $cat['id'];
|
||||||
if(!empty($cat['children']))
|
if(!empty($cat['children']))
|
||||||
{
|
{
|
||||||
$category['item'] = [];
|
unset($category['children']);
|
||||||
unset($cat['children']);
|
static::processCategory($cat['id'], $category['children'], $categories, $globals, $cat_id_list);
|
||||||
static::processCategory($cat['id'], $category['item'], $categories, $globals, $cat_id_list);
|
|
||||||
}
|
}
|
||||||
$options[] = $category;
|
$options[] = $category;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user