diff --git a/api/js/etemplate/Et2Select/Et2Select.ts b/api/js/etemplate/Et2Select/Et2Select.ts
index b03f8724d4..91c01acc8e 100644
--- a/api/js/etemplate/Et2Select/Et2Select.ts
+++ b/api/js/etemplate/Et2Select/Et2Select.ts
@@ -19,6 +19,7 @@ import {property} from "lit/decorators/property.js";
import {SlChangeEvent, SlOption, SlSelect} from "@shoelace-style/shoelace";
import {repeat} from "lit/directives/repeat.js";
import {classMap} from "lit/directives/class-map.js";
+import {state} from "lit/decorators/state.js";
// export Et2WidgetWithSelect which is used as type in other modules
export class Et2WidgetWithSelect extends RowLimitedMixin(Et2WidgetWithSelectMixin(LitElement))
@@ -128,10 +129,12 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
overflow: hidden;
}
- /* Keep overflow tag right-aligned. It's the only sl-tag. */
+ /* No rows set, default height limit about 5 rows */
- ::part(tags) sl-tag {
- margin-left: auto;
+ :host(:not([rows])) ::part(tags) {
+ min-height: inherit;
+ max-height: 11em;
+ overflow-y: auto;
}
select:hover {
@@ -140,20 +143,22 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
/* Hide dropdown trigger when multiple & readonly */
- :host([readonly][multiple])::part(expand-icon) {
+ :host([readonly][multiple]):not([rows="1"])::part(expand-icon) {
display: none;
}
/* Style for tag count if rows=1 */
- :host([readonly][multiple][rows])::part(tags) {
+ .tag_limit {
position: absolute;
right: 0px;
- top: 1px;
+ top: 0px;
+ bottom: 0px;
box-shadow: rgb(0 0 0/50%) -1.5ex 0px 1ex -1ex, rgb(0 0 0 / 0%) 0px 0px 0px 0px;
}
- :host([readonly][multiple][rows]) .select__tags sl-tag::part(base) {
+ .tag_limit::part(base) {
+ height: 100%;
background-color: var(--sl-input-background-color);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
@@ -164,12 +169,16 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
/* Show all rows on hover if rows=1 */
- :host([readonly][multiple][rows]):hover .select__tags {
+ :host([ readonly ][ multiple ][ rows ]) .hover__popup {
width: -webkit-fill-available;
width: -moz-fill-available;
width: fill-available;
}
+ :host([ readonly ][ multiple ][ rows ]) .hover__popup .select__tags {
+ display: flex;
+ flex-wrap: wrap;
+ }
::part(listbox) {
z-index: 1;
background: var(--sl-input-background-color);
@@ -254,15 +263,23 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
/** The select's required attribute. */
@property({type: Boolean, reflect: true}) required = false;
+ /** If the select is limited to 1 row, we show the number of tags not visible */
+ @state()
+ protected _tagsHidden = 0;
private __value : string | string[] = "";
+ protected tagOverflowObserver : IntersectionObserver = null;
+
constructor()
{
super();
this.hoist = true;
this._tagTemplate = this._tagTemplate.bind(this);
+ this._handleMouseEnter = this._handleMouseEnter.bind(this);
+ this._handleMouseLeave = this._handleMouseLeave.bind(this);
+ this._handleTagOverflow = this._handleTagOverflow.bind(this);
}
/**
* List of properties that get translated
@@ -287,6 +304,9 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
this.select?.requestUpdate("value");
// Fixes incorrect opening position
this.select?.popup?.handleAnchorChange();
+
+ // requestUpdate("value") above means we need to check tags again
+ this.select.updateComplete.then(() => {this.checkTagOverflow(); });
});
}
@@ -511,6 +531,49 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
}
}
+ /**
+ * After render, DOM nodes are there
+ *
+ * Check to see if tags overflow, set the counter flag
+ *
+ * @param {PropertyValues} changedProperties
+ */
+ updated(changedProperties : PropertyValues)
+ {
+ super.updated(changedProperties);
+
+ this.checkTagOverflow();
+ }
+
+ protected checkTagOverflow()
+ {
+ // Create / destroy intersection observer
+ if(this.readonly && this.rows == "1" && this.multiple && this.tagOverflowObserver == null)
+ {
+ this.tagOverflowObserver = new IntersectionObserver(this._handleTagOverflow, {
+ root: this.select.shadowRoot.querySelector(".select__tags"),
+ threshold: 0.1
+ });
+ }
+ else if((!this.readonly || this.rows !== "1" || !this.multiple) && this.tagOverflowObserver !== null)
+ {
+ this.tagOverflowObserver.disconnect();
+ this.tagOverflowObserver = null;
+ }
+
+ if(this.tagOverflowObserver)
+ {
+ this.select.updateComplete.then(() =>
+ {
+ // @ts-ignore
+ for(const tag of this.select.shadowRoot.querySelectorAll(".select__tags *:not(div):not(sl-tag)"))
+ {
+ this.tagOverflowObserver.observe(tag);
+ }
+ });
+ }
+ }
+
/**
* Tag used for rendering tags when multiple=true
* Used for creating, finding & filtering options.
@@ -602,6 +665,111 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
}
+ /**
+ * Callback for the intersection observer so we know when tags don't fit
+ *
+ * Here we set the flag to show how many more tags are hidden, but this only happens
+ * when there are more tags than space.
+ *
+ * @param entries
+ * @protected
+ */
+ protected _handleTagOverflow(entries : IntersectionObserverEntry[])
+ {
+ const oldCount = this._tagsHidden;
+ let visibleTagCount = this.value.length - this._tagsHidden;
+ let update = false;
+ // If we have all tags, start from 0, otherwise it's just a change
+ if(entries.length == this.value.length)
+ {
+ visibleTagCount = 0;
+ }
+ else
+ {
+ update = true;
+ }
+ for(const tag of entries)
+ {
+ if(tag.isIntersecting)
+ {
+ visibleTagCount++;
+ }
+ else if(update && !tag.isIntersecting)
+ {
+ visibleTagCount--;
+ }
+ else
+ {
+ break;
+ }
+ }
+ if(visibleTagCount && visibleTagCount < this.value.length)
+ {
+ this._tagsHidden = this.value.length - visibleTagCount;
+ }
+ else
+ {
+ this._tagsHidden = 0;
+ }
+ this.requestUpdate("_tagsHidden", oldCount);
+ }
+
+ /**
+ * If rows=1 and multiple=true, when they put the mouse over the widget show all tags
+ * @param {MouseEvent} e
+ * @private
+ */
+ protected _handleMouseEnter(e : MouseEvent)
+ {
+ if(this.rows == 1 && this.multiple == true && this.value.length > 1)
+ {
+ e.stopPropagation();
+
+ let distance = (-1 * parseInt(getComputedStyle(this).height));
+
+ // Bind to turn this all off
+ this.addEventListener("mouseleave", this._handleMouseLeave);
+
+ // Popup - this might get wiped out next render(), might not
+ this.updateComplete.then(() =>
+ {
+ let tags = this.select.shadowRoot.querySelector(".select__tags");
+ let popup = document.createElement("sl-popup");
+ popup.anchor = this;
+ popup.distance = distance;
+ popup.placement = "bottom";
+ popup.strategy = "fixed";
+ popup.active = true;
+ popup.sync = "width";
+ popup.setAttribute("exportparts", "tags");
+ popup.classList.add("hover__popup", "details", "hoist", "details__body");
+ this.shadowRoot.append(popup);
+ popup.appendChild(tags);
+ tags.style.width = getComputedStyle(this).width;
+ tags.style.margin = 0;
+ });
+ }
+ }
+
+ /**
+ * If we're showing all rows because of _handleMouseEnter, reset when mouse leaves
+ * @param {MouseEvent} e
+ * @private
+ */
+ protected _handleMouseLeave(e : MouseEvent)
+ {
+ let popup = this.shadowRoot.querySelector("sl-popup");
+ if(popup)
+ {
+ // Popup still here. Remove it
+ let tags = popup.firstChild;
+ this.select.shadowRoot.querySelector(".select__combobox").append(tags);
+ popup.remove();
+ }
+ this.removeEventListener("mouseleave", this._handleMouseLeave);
+ this.select.requestUpdate();
+ }
+
/** Shows the listbox. */
async show()
{
@@ -781,6 +949,21 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
`;
}
+ protected _tagLimitTemplate() : TemplateResult | typeof nothing
+ {
+ if(this._tagsHidden == 0)
+ {
+ return nothing;
+ }
+ return html`
+