Work on LinkEntry

This commit is contained in:
nathan 2022-05-31 13:40:31 -06:00
parent c572fa637d
commit 9b0e1b9206
5 changed files with 279 additions and 19 deletions

View File

@ -13,6 +13,7 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
:host { :host {
--icon-width: 20px; --icon-width: 20px;
display: inline-block; display: inline-block;
min-width: 64px;
} }
:host([app_icons]) { :host([app_icons]) {
max-width: 75px; max-width: 75px;
@ -130,6 +131,22 @@ export class Et2LinkAppSelect extends SlotMixin(Et2Select)
} }
} }
set application_list(app_list : string[])
{
let oldValue = this.__application_list;
if(typeof app_list == "string")
{
app_list = (<string>app_list).split(",");
}
this.__application_list = app_list;
this.requestUpdate("application_list", oldValue);
}
get application_list() : string[]
{
return this.__application_list;
}
private _handleChange(e) private _handleChange(e)
{ {
// Set icon // Set icon

View File

@ -0,0 +1,142 @@
import {css, html, LitElement, SlotMixin} from "@lion/core";
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
import {Et2LinkSearch} from "./Et2LinkSearch";
/**
* EGroupware eTemplate2 - Search & select link entry WebComponent
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
export class Et2LinkEntry extends Et2InputWidget(FormControlMixin(ValidateMixin(SlotMixin(LitElement))))
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: block;
}
`
];
}
static get properties()
{
return {
...super.properties,
value: {type: Object},
/**
* Limit to just this application - hides app selection
*/
only_app: {type: String},
/**
* Limit to the listed applications (comma seperated)
*/
application_list: {type: String},
/**
* Show just application icons instead of names
*/
app_icons: {type: Boolean},
/**
* Callback before query to server.
* It will be passed the request & et2_link_entry objects. Must return true, or false to abort query.
*/
query: {type: Function},
/**
* Callback when user selects an option. Must return true, or false to abort normal action.
*/
select: {type: Function}
}
}
get slots()
{
return {
...super.slots,
app: () =>
{
const app = document.createElement("et2-link-apps")
return app;
},
select: () =>
{
const select = <Et2LinkSearch><unknown>document.createElement("et2-link-search");
return select;
}
}
}
constructor()
{
super();
}
connectedCallback()
{
super.connectedCallback();
this._handleAppChange = this._handleAppChange.bind(this);
}
set only_app(app)
{
this._appNode.only_app = app;
this._searchNode.app = app;
}
get only_app()
{
return this._appNode.only_app;
}
get _appNode() : Et2LinkAppSelect
{
return this.querySelector("[slot='app']");
}
get _searchNode() : Et2LinkSearch
{
return this.querySelector("[slot='select']");
}
protected _bindListeners()
{
this._appNode.addEventListener("change", this._handleAppChange);
}
protected _unbindListeners()
{
this._appNode.removeEventListener("change", this._handleAppChange);
}
protected _handleAppChange(event)
{
this._searchNode.app = this._appNode.value;
}
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate()
{
return html`
<div class="input-group__input">
<slot name="app"></slot>
<slot name="select"></slot>
</div>
`;
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-link-entry", Et2LinkEntry);

View File

@ -0,0 +1,72 @@
/**
* EGroupware eTemplate2 - Search & select link entry WebComponent
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @link https://www.egroupware.org
* @author Nathan Gray
*/
import {css} from "@lion/core";
import {Et2Select} from "../Et2Select/Et2Select";
import {Et2LinkAppSelect} from "./Et2LinkAppSelect";
export class Et2LinkSearch extends Et2Select
{
static get styles()
{
return [
...super.styles,
css`
:host {
display: block;
flex: 1 1 auto;
min-width: 200px;
}
::part(icon), .select__icon {
display: none;
}
`
];
}
static get properties()
{
return {
...super.properties,
app: {type: String, reflect: true}
}
}
constructor()
{
super();
this.search = true;
this.searchUrl = "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search";
}
get _appNode() : Et2LinkAppSelect
{
return this.parentNode.querySelector("et2-link-apps");
}
protected remoteQuery(search : string, options : object)
{
let request = this.egw().json(this.searchUrl, [this._appNode.value, '', search, options]);
if(this.query && typeof this.query == "function")
{
if(!this.query(request, this))
{
return;
}
}
request.sendRequest().then((result) =>
{
this.processRemoteResults(result);
});
}
}
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
customElements.define("et2-link-search", Et2LinkSearch);

View File

@ -31,6 +31,12 @@ export declare class SearchMixinInterface
*/ */
allowFreeEntries : boolean; allowFreeEntries : boolean;
/**
* Additional search options passed to the search functions
*
* @type {object}
*/
searchOptions : object;
/** /**
* Start the search process * Start the search process
@ -40,18 +46,18 @@ export declare class SearchMixinInterface
/** /**
* Search local options * Search local options
*/ */
localSearch(search : string) : Promise<void> localSearch(search : string, options : object) : Promise<void>
/** /**
* Search remote options. * Search remote options.
* If searchUrl is not set, it will return very quickly with no results * If searchUrl is not set, it will return very quickly with no results
*/ */
remoteSearch(search : string) : Promise<void> remoteSearch(search : string, options : object) : Promise<void>
/** /**
* Check a [local] item to see if it matches * Check a [local] item to see if it matches
*/ */
searchMatch(search : string, item : LitElement) : boolean searchMatch(search : string, options : object, item : LitElement) : boolean
} }
/** /**
@ -72,7 +78,9 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
searchUrl: {type: String}, searchUrl: {type: String},
allowFreeEntries: {type: Boolean} allowFreeEntries: {type: Boolean},
searchOptions: {type: Object}
} }
} }
@ -110,7 +118,7 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
width: 100%; width: 100%;
} }
::slotted(.search_input.active) { ::slotted(.search_input.active) {
display: block; display: flex;
} }
::slotted(.no-match) { ::slotted(.no-match) {
display: none; display: none;
@ -120,13 +128,17 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
} }
private _searchTimeout : number; private _searchTimeout : number;
protected static SEARCH_DELAY = 200; protected static SEARCH_TIMEOUT = 500;
protected static MIN_CHARS = 2; protected static MIN_CHARS = 2;
constructor(...args : any[]) constructor(...args : any[])
{ {
super(...args); super(...args);
this.search = false;
this.searchUrl = "";
this.searchOptions = {};
this._handleSearchButtonClick = this._handleSearchButtonClick.bind(this); this._handleSearchButtonClick = this._handleSearchButtonClick.bind(this);
this._handleSearchAbort = this._handleSearchAbort.bind(this); this._handleSearchAbort = this._handleSearchAbort.bind(this);
this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this); this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this);
@ -176,6 +188,16 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
} }
/**
* Do we have the needed properties set, so we can actually do searching
*
* @returns {boolean}
*/
public get searchEnabled() : boolean
{
return this.search || this.searchUrl.length > 0;
}
protected get _searchButtonNode() protected get _searchButtonNode()
{ {
return this.querySelector("sl-icon[slot='suffix']"); return this.querySelector("sl-icon[slot='suffix']");
@ -232,15 +254,21 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
{ {
super.handleMenuShow(); super.handleMenuShow();
this._activeControls.classList.add("active"); if(this.searchEnabled)
{
this._activeControls?.classList.add("active");
this._searchInputNode.focus(); this._searchInputNode.focus();
this._searchInputNode.select(); this._searchInputNode.select();
} }
}
handleMenuHide() handleMenuHide()
{ {
super.handleMenuHide(); super.handleMenuHide();
this._activeControls.classList.remove("active"); if(this.searchEnabled)
{
this._activeControls?.classList.remove("active");
}
} }
/** /**
@ -250,7 +278,7 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
*/ */
protected _handleSearchKeyDown(event : KeyboardEvent) protected _handleSearchKeyDown(event : KeyboardEvent)
{ {
this._activeControls.classList.add("active"); this._activeControls?.classList.add("active");
this.dropdown.show(); this.dropdown.show();
// Pass off some keys to select // Pass off some keys to select
@ -278,7 +306,7 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
clearTimeout(this._searchTimeout); clearTimeout(this._searchTimeout);
if(this._searchInputNode.value.length >= Et2WidgetWithSearch.MIN_CHARS) if(this._searchInputNode.value.length >= Et2WidgetWithSearch.MIN_CHARS)
{ {
this._searchTimeout = window.setTimeout(() => {this.startSearch()}, Et2WidgetWithSearch.SEARCH_DELAY); this._searchTimeout = window.setTimeout(() => {this.startSearch()}, Et2WidgetWithSearch.SEARCH_TIMEOUT);
} }
} }
@ -298,8 +326,8 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
// Start the searches // Start the searches
Promise.all([ Promise.all([
this.localSearch(this._searchInputNode.value), this.localSearch(this._searchInputNode.value, this.searchOptions),
this.remoteSearch(this._searchInputNode.value) this.remoteSearch(this._searchInputNode.value, this.searchOptions)
]).then(() => ]).then(() =>
{ {
spinner.remove(); spinner.remove();
@ -312,7 +340,7 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
* @param {string} search * @param {string} search
* @protected * @protected
*/ */
protected localSearch(search : string) : Promise<void> protected localSearch(search : string, options : object) : Promise<void>
{ {
return new Promise((resolve) => return new Promise((resolve) =>
{ {
@ -333,7 +361,7 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
* @param {string} search * @param {string} search
* @protected * @protected
*/ */
protected remoteSearch(search : string) protected remoteSearch(search : string, options : object)
{ {
// Remove existing remote items // Remove existing remote items
this.remoteItems.forEach(i => i.remove()); this.remoteItems.forEach(i => i.remove());
@ -344,16 +372,15 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
} }
// Fire off the query // Fire off the query
let promise = this.remoteQuery(search); let promise = this.remoteQuery(search, options);
return promise; return promise;
} }
protected remoteQuery(search : string) protected remoteQuery(search : string, options : object)
{ {
return this.egw().json(this.searchUrl, [search]).sendRequest().then((result) => return this.egw().json(this.searchUrl, [search]).sendRequest().then((result) =>
{ {
debugger;
this.processRemoteResults(result); this.processRemoteResults(result);
}); });
} }

View File

@ -44,7 +44,9 @@ import './Et2Favorites/Et2Favorites';
import './Et2Image/Et2Image'; import './Et2Image/Et2Image';
import './Et2Link/Et2Link'; import './Et2Link/Et2Link';
import './Et2Link/Et2LinkAppSelect'; import './Et2Link/Et2LinkAppSelect';
import './Et2Link/Et2LinkEntry';
import './Et2Link/Et2LinkList'; import './Et2Link/Et2LinkList';
import './Et2Link/Et2LinkSearch';
import './Et2Link/Et2LinkString'; import './Et2Link/Et2LinkString';
import './Et2Select/Et2Select'; import './Et2Select/Et2Select';
import './Et2Select/Et2SelectAccount'; import './Et2Select/Et2SelectAccount';