From a3d2674757e42bfbcc6ab50e44d0d53f33250f60 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 31 Aug 2022 13:27:10 -0600 Subject: [PATCH] Et2Select + search: Fix some bugs - Select a searched value didn't validate due to different attribute name - et2-searchbox inside et2-select threw an additional change event, needed to catch that - fix_bad_value() needs different handling when you can search, otherwise it just gets cleared again --- api/js/etemplate/Et2Select/Et2Select.ts | 10 +++- api/js/etemplate/Et2Select/SearchMixin.ts | 59 ++++++++++++++++++++++- api/src/Etemplate/Widget/Taglist.php | 12 +++-- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/api/js/etemplate/Et2Select/Et2Select.ts b/api/js/etemplate/Et2Select/Et2Select.ts index 4999e04cbd..de385aa516 100644 --- a/api/js/etemplate/Et2Select/Et2Select.ts +++ b/api/js/etemplate/Et2Select/Et2Select.ts @@ -193,7 +193,10 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) _triggerChange(e) { - this.dispatchEvent(new Event("change")); + if(super._triggerChange(e)) + { + this.dispatchEvent(new Event("change")); + } } /** @@ -234,6 +237,11 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) // Nothing to do here return; } + // See if parent (search / free entry) is OK with it + if(super.fix_bad_value()) + { + return; + } // If no value is set, choose the first option // Only do this on once during initial setup, or it can be impossible to clear the value const valueArray = Array.isArray(this.value) ? this.value : (!this.value ? [] : this.value.toString().split(',')); diff --git a/api/js/etemplate/Et2Select/SearchMixin.ts b/api/js/etemplate/Et2Select/SearchMixin.ts index b159543f41..876a440c6e 100644 --- a/api/js/etemplate/Et2Select/SearchMixin.ts +++ b/api/js/etemplate/Et2Select/SearchMixin.ts @@ -140,6 +140,10 @@ export const Et2WithSearchMixin = >(superclass 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 */ @@ -265,6 +269,7 @@ export const Et2WithSearchMixin = >(superclass this._handleClear = this._handleClear.bind(this); this._handleDoubleClick = this._handleDoubleClick.bind(this); this._handleSearchAbort = this._handleSearchAbort.bind(this); + this._handleSearchChange = this._handleSearchChange.bind(this); this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this); this._handleEditKeyDown = this._handleEditKeyDown.bind(this); } @@ -393,12 +398,12 @@ export const Et2WithSearchMixin = >(superclass protected get _searchInputNode() : HTMLInputElement { - return this._activeControls.querySelector("#search"); + return this._activeControls?.querySelector("#search"); } protected get _editInputNode() : HTMLInputElement { - return this._activeControls.querySelector("input#edit"); + return this._activeControls?.querySelector("input#edit"); } protected get _activeControls() @@ -454,6 +459,25 @@ export const Et2WithSearchMixin = >(superclass } } + protected fix_bad_value() + { + if(!this.allowFreeEntries && !this.searchEnabled) + { + // Let regular select deal with it + return false; + } + 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) + { + return false; + } + + return true; + // TODO? Should we check the server, or just be OK with it? Passing the "current" value in sel_options makes sure the value is there + } + protected _bindListeners() { this.addEventListener("sl-select", this._handleSelect); @@ -468,6 +492,9 @@ export const Et2WithSearchMixin = >(superclass // selecting an option fires 2 change events - 1 before the widget is finished adjusting, losing the value // We catch all change events, then call this._oldChange only when value changes this.removeEventListener("change", this._oldChange); + + this._searchInputNode.removeEventListener("change", this._searchInputNode.handleChange); + this._searchInputNode.addEventListener("change", this._handleSearchChange); }); } @@ -476,6 +503,8 @@ export const Et2WithSearchMixin = >(superclass this.removeEventListener("sl-select", this._handleSelect); this.removeEventListener("sl-clear", this._handleClear) this.removeEventListener("change", this._handleChange); + + this._searchInputNode?.removeEventListener("change", this._handleSearchChange); } handleMenuShow() @@ -523,6 +552,18 @@ export const Et2WithSearchMixin = >(superclass } } + _triggerChange(event) + { + // Don't want searchbox events to trigger change event + if(event.target == this._searchInputNode) + { + event.stopImmediatePropagation(); + event.preventDefault(); + return false; + } + return true; + } + _handleChange(event) { if(event.target == this._searchInputNode) @@ -792,6 +833,7 @@ export const Et2WithSearchMixin = >(superclass render(html`${repeat(entries, (option : SelectOption) => option.value, this._optionTemplate.bind(this))}`, target ); + this.handleMenuSlotChange(); } } @@ -951,6 +993,19 @@ export const Et2WithSearchMixin = >(superclass }) this.syncItemsFromValue(); } + + /** + * et2-searchbox (SlInput) sends out an event on change. + * We don't care, and if we let it bubble it'll get in the way. + * @param e + * @protected + */ + protected _handleSearchChange(e) + { + e.stopImmediatePropagation(); + e.preventDefault(); + return false; + } } return Et2WidgetWithSearch as unknown as Constructor & T; diff --git a/api/src/Etemplate/Widget/Taglist.php b/api/src/Etemplate/Widget/Taglist.php index 93c3f2f643..39b42a675f 100644 --- a/api/src/Etemplate/Widget/Taglist.php +++ b/api/src/Etemplate/Widget/Taglist.php @@ -152,19 +152,23 @@ class Taglist extends Etemplate\Widget $type == 2 && $account_type == 'users' || in_array($account_type, array('owngroups', 'memberships')) && !in_array($val, $GLOBALS['egw']->accounts->memberships( - $GLOBALS['egw_info']['user']['account_id'], true)) + $GLOBALS['egw_info']['user']['account_id'], true + ) + ) ) { self::set_validation_error($form_name, lang("'%1' is NOT allowed ('%2')!", $val, - !$type?'not found' : ($type == 1 ? 'user' : 'group')),''); + !$type ? 'not found' : ($type == 1 ? 'user' : 'group') + ), '' + ); $value = ''; break; } continue; } - if(count($allowed) && !$this->attrs['allowFreeEntries'] && empty($this->attrs['autocomplete_url']) && !array_key_exists($val,$allowed)) + if(count($allowed) && !$this->attrs['allowFreeEntries'] && empty($this->attrs['searchUrl']) && !array_key_exists($val, $allowed)) { - self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!",$val,implode("','",array_keys($allowed))),''); + self::set_validation_error($form_name, lang("'%1' is NOT allowed ('%2')!", $val, implode("','", array_keys($allowed))), ''); unset($value[$key]); } if(str_contains($this->type, 'email') && $this->attrs['include_lists'] && is_numeric($val))