diff --git a/api/js/etemplate/Et2InputWidget/test/InputBasicTests.ts b/api/js/etemplate/Et2InputWidget/test/InputBasicTests.ts index 29a0dc00be..bc398c489d 100644 --- a/api/js/etemplate/Et2InputWidget/test/InputBasicTests.ts +++ b/api/js/etemplate/Et2InputWidget/test/InputBasicTests.ts @@ -82,11 +82,21 @@ export function inputBasicTests(before : Function, test_value : string, value_se { element = await before(); }); - it("no value gives empty string", () => + it("no value gives empty string", async() => { + element.set_value(""); + await elementUpdated(element); + // Shows as empty / no value let value = (element).querySelector(value_selector) || (element).shadowRoot.querySelector(value_selector); + assert.isDefined(value, "Bad value selector '" + value_selector + "'"); + debugger; assert.equal(value.textContent.trim(), "", "Displaying something when there is no value"); + if(element.multiple) + { + assert.isEmpty(element.get_value()); + return; + } // Gives no value assert.equal(element.get_value(), "", "Value mismatch"); }); @@ -94,7 +104,7 @@ export function inputBasicTests(before : Function, test_value : string, value_se it("value out matches value in", async() => { element.set_value(test_value); - + debugger; // wait for asychronous changes to the DOM await elementUpdated(element); diff --git a/api/js/etemplate/Et2Select/Et2Select.ts b/api/js/etemplate/Et2Select/Et2Select.ts index 62c8d2cf96..2430a89fc1 100644 --- a/api/js/etemplate/Et2Select/Et2Select.ts +++ b/api/js/etemplate/Et2Select/Et2Select.ts @@ -14,7 +14,6 @@ import {Et2WidgetWithSelectMixin} from "./Et2WidgetWithSelectMixin"; import {SelectOption} from "./FindSelectOptions"; import shoelace from "../Styles/shoelace"; import {RowLimitedMixin} from "../Layout/RowLimitedMixin"; -import {Et2Tag} from "./Tag/Et2Tag"; import {Et2WithSearchMixin} from "./SearchMixin"; import {property} from "lit/decorators/property.js"; import {SlChangeEvent, SlOption, SlSelect} from "@shoelace-style/shoelace"; @@ -582,61 +581,6 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) return literal`et2-tag`; } - - /** - * Customise how tags are rendered. This overrides what SlSelect - * does in syncItemsFromValue(). - * This is a copy+paste from SlSelect.syncItemsFromValue(). - * - * @param item - * @protected - */ - protected _createTagNode(item) - { - console.warn("Deprecated"); - debugger; - let tag; - if(typeof super._createTagNode == "function") - { - tag = super._createTagNode(item); - } - else - { - tag = document.createElement(this.tagTag); - } - tag.value = item.value; - tag.textContent = item?.getTextLabel()?.trim(); - tag.class = item.classList.value + " search_tag"; - tag.setAttribute("exportparts", "icon"); - if(this.size) - { - tag.size = this.size; - } - if(this.readonly || item.option && typeof (item.option.disabled) != "undefined" && item.option.disabled) - { - tag.removable = false; - tag.readonly = true; - } - else - { - tag.addEventListener("dblclick", this._handleDoubleClick); - tag.addEventListener("click", this.handleTagInteraction); - tag.addEventListener("keydown", this.handleTagInteraction); - tag.addEventListener("sl-remove", (event : CustomEvent) => this.handleTagRemove(event, item)); - } - // Allow click handler even if read only - if(typeof this.onTagClick == "function") - { - tag.addEventListener("click", (e) => this.onTagClick(e, e.target)); - } - let image = this._createImage(item); - if(image) - { - tag.prepend(image); - } - return tag; - } - blur() { if(typeof super.blur == "function") @@ -727,37 +671,6 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) } - /** - * Get the icon for the select option - * - * @param option - * @protected - */ - protected _iconTemplate(option) - { - if(!option.icon) - { - return html``; - } - - return html` - ` - } - - protected _createImage(item) - { - let image = item?.querySelector ? item.querySelector("et2-image") || item.querySelector("[slot='prefix']") : null; - if(image) - { - image = image.clone(); - image.slot = "prefix"; - image.class = "tag_image"; - return image; - } - return ""; - } - /** Shows the listbox. */ async show() { @@ -865,6 +778,24 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) `; } + /** + * Get the icon for the select option + * + * @param option + * @protected + */ + protected _iconTemplate(option) + { + if(!option.icon) + { + return html``; + } + + return html` + ` + } + /** * Custom tag @@ -881,7 +812,7 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) { const readonly = (this.readonly || option && typeof (option.disabled) != "undefined" && option.disabled); const isEditable = this.editModeEnabled && !readonly; - const image = this._createImage(option); + const image = this._iconTemplate(option); const tagName = this.tagTag; return html` <${tagName} @@ -900,6 +831,7 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) ?readonly=${readonly} ?editable=${isEditable} .value=${option.value.replaceAll("___", " ")} + @change=${this.handleTagEdit} @dblclick=${this._handleDoubleClick} @click=${typeof this.onTagClick == "function" ? (e) => this.onTagClick(e, e.target) : nothing} > diff --git a/api/js/etemplate/Et2Select/SearchMixin.ts b/api/js/etemplate/Et2Select/SearchMixin.ts index 73f95f07dc..bf003440e9 100644 --- a/api/js/etemplate/Et2Select/SearchMixin.ts +++ b/api/js/etemplate/Et2Select/SearchMixin.ts @@ -751,8 +751,10 @@ export const Et2WithSearchMixin = dedupeMixin( focus() { - this.show(); - this._searchInputNode.focus(); + this.show().then(() => + { + this._searchInputNode?.focus(); + }); } _handleMenuHide() @@ -1303,20 +1305,16 @@ export const Et2WithSearchMixin = dedupeMixin( } // Make sure not to double-add, but wait until the option is there - this.updateComplete.then(() => + if(this.multiple && this.getValueAsArray().indexOf(text) == -1) { - if(this.multiple && this.getValueAsArray().indexOf(text) == -1) - { - let value = this.getValueAsArray(); - value.push(text); - this.value = value; - } - else if(!this.multiple && this.value !== text) - { - this.value = text; - } - this.requestUpdate("value"); - }); + let value = this.getValueAsArray(); + value.push(text); + this.value = value; + } + else if(!this.multiple && this.value !== text) + { + this.value = text; + } // If we were overlapping edit inputbox with the value display, reset if(!this.readonly && this._activeControls?.classList.contains("novalue")) @@ -1370,7 +1368,6 @@ export const Et2WithSearchMixin = dedupeMixin( { this.value = value; } - this.querySelector("[value='" + original.replace(/'/g, "\\\'") + "']")?.remove(); this.__select_options = this.__select_options.filter(v => v.value !== original); } } diff --git a/api/js/etemplate/Et2Select/test/EditableTag.test.ts b/api/js/etemplate/Et2Select/test/EditableTag.test.ts index 20c649c4fa..c6521e4aac 100644 --- a/api/js/etemplate/Et2Select/test/EditableTag.test.ts +++ b/api/js/etemplate/Et2Select/test/EditableTag.test.ts @@ -13,6 +13,7 @@ window.egw = { }; let element : Et2Select; +const tag_name = "et2-tag"; async function before(editable = true) { @@ -20,16 +21,19 @@ async function before(editable = true) // @ts-ignore element = await fixture(html` - One - Two + + `); + // Need to call loadFromXML() explicitly to read the options + element.loadFromXML(element); + // Stub egw() sinon.stub(element, "egw").returns(window.egw); await element.updateComplete; let tags = []; - element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tags.push(t.updateComplete)); + element.shadowRoot.querySelectorAll(tag_name).forEach((t : Et2Tag) => tags.push(t.updateComplete)); await Promise.all(tags); return element; @@ -48,30 +52,29 @@ describe("Editable tag", () => it("Tag editable matches editModeEnabled", async() => { - let tag = element.shadowRoot.querySelectorAll(element.tagTag); + let tag = element.select.combobox.querySelectorAll(tag_name); assert.isAbove(tag.length, 0, "No tags found"); assert.isTrue(tag[0].editable); // Change it to false & force immediate update element.editModeEnabled = false; - element.syncItemsFromValue(); element.requestUpdate(); await element.updateComplete; - tag = element.shadowRoot.querySelectorAll(element.tagTag); + tag = element.select.combobox.querySelectorAll(tag_name); assert.isAbove(tag.length, 0, "No tags found"); assert.isFalse(tag[0].editable); }); it("Has edit button when editable ", async() => { - let tag = element.shadowRoot.querySelectorAll(element.tagTag); + let tag = element.select.combobox.querySelectorAll(tag_name); assert.isAbove(tag.length, 0, "No tags found"); assert.exists(tag[0].shadowRoot.querySelector("et2-button-icon[label='edit*']"), "No edit button"); }); it("Shows input when edit button is clicked", async() => { - let tag = element.shadowRoot.querySelectorAll(element.tagTag)[0]; + let tag = element.select.combobox.querySelectorAll(tag_name)[0]; let edit_button = tag.shadowRoot.querySelector("et2-button-icon"); edit_button.click(); @@ -81,7 +84,7 @@ describe("Editable tag", () => }); it("Changes value when edited", async() => { - let tag = element.shadowRoot.querySelectorAll(element.tagTag)[0]; + let tag = element.select.combobox.querySelectorAll(tag_name)[0]; tag.isEditing = true; tag.requestUpdate(); await tag.updateComplete; @@ -119,7 +122,7 @@ describe("Editable tag", () => await listener2; assert.equal(tag.value, "change select too"); - // Haven't turned on allow free entries, so no change here + // Have turned on allow free entries, so it should change here assert.equal(element.value, "change select too", "Tag change did not cause value change in parent select (allowFreeEntries was on)"); }); @@ -129,7 +132,7 @@ describe("Editable tag", () => element.readonly = true; await element.updateComplete; - let tag = element.shadowRoot.querySelectorAll(element.tagTag); + let tag = element.select.combobox.querySelectorAll(tag_name); assert.isAbove(tag.length, 0, "No tags found"); let wait = []; @@ -146,7 +149,7 @@ describe("Select is not editable", () => it("Does not have edit button when not editable", async() => { - let tag = element.shadowRoot.querySelectorAll(element.tagTag); + let tag = element.select.combobox.querySelectorAll(tag_name); assert.isAbove(tag.length, 0, "No tags found"); assert.isNull(tag[0].shadowRoot.querySelector("et2-button-icon[label='edit*']"), "Unexpected edit button"); diff --git a/api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts b/api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts index 8c36db5cfa..77a1af7c3e 100644 --- a/api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts +++ b/api/js/etemplate/Et2Select/test/Et2EmailTag.test.ts @@ -9,6 +9,18 @@ import {assert, fixture, html} from '@open-wc/testing'; import {Et2EmailTag} from "../Tag/Et2EmailTag"; +import * as sinon from 'sinon'; + +// Stub global egw +// @ts-ignore +window.egw = { + tooltipUnbind: () => {}, + lang: i => i + "*", + image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=", + webserverUrl: "", + app: (_app) => _app, + jsonq: () => Promise.resolve({}) +}; describe('Et2EmailTag', () => { @@ -18,7 +30,13 @@ describe('Et2EmailTag', () => { component = await fixture(html` `); + // Stub egw() + // @ts-ignore + sinon.stub(component, "egw").returns(window.egw); await component.updateComplete; + + // Asserting this instanceOf forces class loading + assert.instanceOf(component, Et2EmailTag); }); it('should be defined', () => @@ -48,7 +66,8 @@ describe('Et2EmailTag', () => it('should open addressbook with email preset on (+) click', () => { - component.egw = () => ({ + window.egw.open = () => + { open: (url, app, mode, extra) => { assert.equal(url, ''); @@ -56,7 +75,7 @@ describe('Et2EmailTag', () => assert.equal(mode, 'add'); assert.equal(extra['presets[email]'], 'test@example.com'); } - }); + }; component.handleMouseDown(new MouseEvent('click')); }); @@ -70,7 +89,8 @@ describe('Et2EmailTag', () => }; component.value = 'test@example.com'; component.checkContact = async(email) => contact; - component.egw = () => ({ + component.egw.open = () => + { open: (id, app, mode, extra) => { assert.equal(id, contact.id); @@ -78,7 +98,7 @@ describe('Et2EmailTag', () => assert.equal(mode, 'view'); assert.deepEqual(extra, {title: contact.n_fn, icon: contact.photo}); } - }); + }; await component.handleContactMouseDown(new MouseEvent('click')); }); }); diff --git a/api/js/etemplate/Et2Select/test/Et2SelectBasic.test.ts b/api/js/etemplate/Et2Select/test/Et2SelectBasic.test.ts index 825d072ed2..c9bd9c4987 100644 --- a/api/js/etemplate/Et2Select/test/Et2SelectBasic.test.ts +++ b/api/js/etemplate/Et2Select/test/Et2SelectBasic.test.ts @@ -25,11 +25,13 @@ async function before() // Create an element to test with, and wait until it's ready // @ts-ignore element = await fixture(html` - + + `); // Stub egw() sinon.stub(element, "egw").returns(window.egw); + await elementUpdated(element); return element; } @@ -48,7 +50,6 @@ describe("Select widget basics", () => it('has a label', async() => { element.set_label("Label set"); - // @ts-ignore TypeScript doesn't recognize widgets as Elements await elementUpdated(element); assert.equal(element.querySelector("[slot='label']").textContent, "Label set"); @@ -64,28 +65,30 @@ describe("Select widget basics", () => { // WIP const blurSpy = sinon.spy(); - element.addEventListener('blur', blurSpy); + element.addEventListener('sl-hide', blurSpy); const showPromise = new Promise(resolve => { element.addEventListener("sl-after-show", resolve); }); const hidePromise = new Promise(resolve => { - element.addEventListener("blur", resolve); + element.addEventListener("sl-hide", resolve); }); - + await elementUpdated(element); element.focus(); await showPromise; + await elementUpdated(element); element.blur(); + await elementUpdated(element); await hidePromise; sinon.assert.calledOnce(blurSpy); // Check that it actually closed dropdown - assert.isFalse(element.dropdown?.hasAttribute("open")); + assert.isFalse(element.select?.hasAttribute("open")); }) }); @@ -97,10 +100,11 @@ describe("Multiple", () => // @ts-ignore element = await fixture(html` - One - Two + + `); + element.loadFromXML(element); element.set_value("one,two"); // Stub egw() @@ -111,14 +115,14 @@ describe("Multiple", () => it("Can remove tags", async() => { - assert.equal(element.querySelectorAll("sl-option").length, 2, "Did not find options"); + assert.equal(element.select.querySelectorAll("sl-option").length, 2, "Did not find options"); assert.sameMembers(element.value, ["one", "two"]); - let tags = element.shadowRoot.querySelectorAll('.select__tags > *'); + let tags = element.select.combobox.querySelectorAll('.select__tags et2-tag'); // Await tags to render let tag_updates = [] - element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tag_updates.push(t.updateComplete)); + element.select.combobox.querySelectorAll("et2-tag").forEach((t : Et2Tag) => tag_updates.push(t.updateComplete)); await Promise.all(tag_updates); assert.equal(tags.length, 2); @@ -138,15 +142,21 @@ describe("Multiple", () => // Wait for widget to update await element.updateComplete; tag_updates = [] - element.shadowRoot.querySelectorAll(element.tagTag).forEach((t : Et2Tag) => tag_updates.push(t.updateComplete)); + element.select.combobox.querySelectorAll('et2-tag').forEach((t : Et2Tag) => tag_updates.push(t.updateComplete)); await Promise.all(tag_updates); // Check assert.sameMembers(element.value, ["two"], "Removing tag did not remove value"); - tags = element.shadowRoot.querySelectorAll('.select__tags > *'); + tags = element.select.combobox.querySelectorAll('.select__tags et2-tag'); assert.equal(tags.length, 1, "Removed tag is still there"); }); }); -inputBasicTests(before, "", "select"); \ No newline at end of file +inputBasicTests(async() => +{ + const element = await before(); + element.noLang = true; + element.select_options = [{value: "", label: ""}]; + return element +}, "", "sl-select"); \ No newline at end of file diff --git a/api/js/etemplate/Et2Select/test/Et2SelectOptions.test.ts b/api/js/etemplate/Et2Select/test/Et2SelectOptions.test.ts index dcfd397319..32b3045e0a 100644 --- a/api/js/etemplate/Et2Select/test/Et2SelectOptions.test.ts +++ b/api/js/etemplate/Et2Select/test/Et2SelectOptions.test.ts @@ -1,8 +1,9 @@ import {assert, elementUpdated, fixture, html} from '@open-wc/testing'; import {Et2Box} from "../../Layout/Et2Box/Et2Box"; -import {Et2Select, SelectOption} from "../Et2Select"; +import {Et2Select} from "../Et2Select"; import * as sinon from "sinon"; import {et2_arrayMgr} from "../../et2_core_arrayMgr"; +import {SelectOption} from "../FindSelectOptions"; let parser = new window.DOMParser(); @@ -30,7 +31,6 @@ describe("Select widget", () => beforeEach(async() => { // This stuff because otherwise Et2Select isn't actually loaded when testing - // @ts-ignore TypeScript is not recognizing that this widget is a LitElement element = await fixture(html` `); @@ -39,7 +39,6 @@ describe("Select widget", () => assert.instanceOf(element, Et2Select); element.remove(); - // @ts-ignore TypeScript is not recognizing that this widget is a LitElement container = await fixture(html` `);