mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 06:30:59 +01:00
Et2Email: Readonly & limited rows styling
This commit is contained in:
parent
57c76e9840
commit
a673a6ac5f
@ -27,7 +27,7 @@ export default css`
|
||||
border-radius: var(--sl-input-border-radius-medium);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
min-height: var(--sl-input-height-medium);
|
||||
max-height: calc(var(--height, 2.5) * var(--sl-input-height-medium));
|
||||
max-height: calc(var(--height, 5) * var(--sl-input-height-medium));
|
||||
overflow-y: auto;
|
||||
padding-block: 0;
|
||||
padding-inline: var(--sl-input-spacing-medium);
|
||||
@ -58,7 +58,8 @@ export default css`
|
||||
order: 1;
|
||||
}
|
||||
/* Tags */
|
||||
.email et2-email-tag {
|
||||
|
||||
et2-email-tag {
|
||||
order: 2;
|
||||
flex-grow: 0;
|
||||
margin: auto 0px;
|
||||
@ -137,4 +138,70 @@ export default css`
|
||||
padding-inline: var(--sl-spacing-x-large);
|
||||
}
|
||||
|
||||
/**
|
||||
* Readonly
|
||||
*/
|
||||
|
||||
:host([readonly]) .email .email__combobox {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
max-height: calc(var(--height, 5) * (var(--sl-input-height-medium) * 0.8))
|
||||
}
|
||||
|
||||
:host([readonly])::part(expand-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([readonly]) .email__search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Style for tag count if readonly and rows=1
|
||||
*/
|
||||
|
||||
:host([readonly][rows="1"]) .email__combobox {
|
||||
overflow: hidden;
|
||||
min-height: auto;
|
||||
max-height: calc(var(--sl-input-height-medium) * 0.8);
|
||||
}
|
||||
|
||||
.tag_limit {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
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;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tag_limit::part(base) {
|
||||
height: 100%;
|
||||
background-color: var(--sl-input-background-color);
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
font-weight: bold;
|
||||
min-width: 3em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Show all rows on hover if readonly rows=1 */
|
||||
|
||||
:host([ readonly ][ rows ]) .hover__popup {
|
||||
width: -webkit-fill-available;
|
||||
width: -moz-fill-available;
|
||||
width: fill-available;
|
||||
}
|
||||
|
||||
:host([readonly][rows]) .hover__popup::part(popup) {
|
||||
z-index: var(--sl-z-index-dropdown);
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
/* Same as .email__combobox */
|
||||
gap: 0.1rem 0.5rem;
|
||||
}
|
||||
|
||||
/* End styles for [readonly][rows=1] */
|
||||
`;
|
@ -11,6 +11,7 @@ import {html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {state} from "lit/decorators/state.js";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import {styleMap} from "lit/directives/style-map.js";
|
||||
import {keyed} from "lit/directives/keyed.js";
|
||||
import {live} from "lit/directives/live.js";
|
||||
import {map} from "lit/directives/map.js";
|
||||
@ -63,7 +64,7 @@ import Sortable from "sortablejs/modular/sortable.complete.esm.js";
|
||||
* @csspart option - Each matching email address suggestion
|
||||
* @csspart tag - The individual tags that represent each email address.
|
||||
*
|
||||
* @cssproperty [--height=2.5] - The maximum height of the widget, to limit size when you have a lot of addresses.
|
||||
* @cssproperty [--height=5] - The maximum height of the widget, to limit size when you have a lot of addresses. Set by rows property, when set.
|
||||
*/
|
||||
export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinInterface
|
||||
{
|
||||
@ -144,11 +145,21 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
*/
|
||||
@property({type: String}) searchUrl = "EGroupware\\Api\\Etemplate\\Widget\\Taglist::ajax_email";
|
||||
|
||||
/**
|
||||
* Limit the maximum height of the widget, for when you have a lot of addresses.
|
||||
* Set it to 1 for special single-line styling, 0 to disable
|
||||
* @type {number}
|
||||
*/
|
||||
@property({type: Number, reflect: true}) rows;
|
||||
|
||||
@state() searching = false;
|
||||
@state() hasFocus = false;
|
||||
@state() currentOption : SlOption;
|
||||
@state() currentTag : Et2EmailTag;
|
||||
|
||||
/** If the select is limited to 1 row, we show the number of tags not visible */
|
||||
@state() _tagsHidden = 0;
|
||||
|
||||
|
||||
get _popup() : SlPopup { return this.shadowRoot.querySelector("sl-popup");}
|
||||
|
||||
@ -187,6 +198,9 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
protected _searchPromise : Promise<SelectOption[]> = Promise.resolve([]);
|
||||
protected _selectOptions : SelectOption[] = [];
|
||||
|
||||
// Overflow Observer for +# display
|
||||
protected tagOverflowObserver : IntersectionObserver = null;
|
||||
|
||||
// Drag / drop / sort
|
||||
protected _sortable : Sortable;
|
||||
|
||||
@ -208,8 +222,10 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
|
||||
this.handleOpenChange = this.handleOpenChange.bind(this);
|
||||
this.handleLostFocus = this.handleLostFocus.bind(this);
|
||||
|
||||
this.handleSortEnd = this.handleSortEnd.bind(this);
|
||||
this.handleTagOverflow = this.handleTagOverflow.bind(this);
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback()
|
||||
@ -271,6 +287,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
{
|
||||
this.makeSortable();
|
||||
}
|
||||
this.checkTagOverflow();
|
||||
}
|
||||
|
||||
private _getEmailDisplayPreference()
|
||||
@ -374,6 +391,35 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
}
|
||||
}
|
||||
|
||||
protected checkTagOverflow()
|
||||
{
|
||||
// Create / destroy intersection observer
|
||||
if(this.readonly && this.rows == "1" && this.tagOverflowObserver == null)
|
||||
{
|
||||
this.tagOverflowObserver = new IntersectionObserver(this.handleTagOverflow, {
|
||||
root: this.shadowRoot.querySelector(".email__combobox"),
|
||||
threshold: 0.1
|
||||
});
|
||||
}
|
||||
else if((!this.readonly || this.rows !== 1) && this.tagOverflowObserver !== null)
|
||||
{
|
||||
this.tagOverflowObserver.disconnect();
|
||||
this.tagOverflowObserver = null;
|
||||
}
|
||||
|
||||
if(this.tagOverflowObserver)
|
||||
{
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
for(const tag of Array.from(this.shadowRoot.querySelectorAll(".email__combobox et2-email-tag")))
|
||||
{
|
||||
this.tagOverflowObserver.observe(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create an entry that is not in the suggestions and add it to the value
|
||||
*
|
||||
@ -667,6 +713,56 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sometimes users paste multiple comma separated values at once. Split them then handle normally.
|
||||
* Overridden here to handle email addresses that may have commas using the regex from the validator.
|
||||
@ -895,6 +991,36 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.value.length > 1)
|
||||
{
|
||||
e.stopPropagation();
|
||||
|
||||
// Bind to turn this all off
|
||||
this.addEventListener("mouseleave", this.handleMouseLeave);
|
||||
|
||||
this.classList.add("hover");
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're showing all rows because of _handleMouseEnter, reset when mouse leaves
|
||||
* @param {MouseEvent} e
|
||||
* @private
|
||||
*/
|
||||
protected handleMouseLeave(e : MouseEvent)
|
||||
{
|
||||
this.classList.remove("hover");
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard events from the suggestion list
|
||||
*
|
||||
@ -1013,6 +1139,31 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
this.dispatchEvent(new Event("change", {bubbles: true}));
|
||||
}
|
||||
|
||||
/* Sub-template when [readonly][rows=1] to show all tags in current value in popup */
|
||||
readonlyHoverTemplate()
|
||||
{
|
||||
if(!this.classList.contains("hover"))
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Offset distance to open _over_ the rest
|
||||
let distance = (-1 * parseInt(getComputedStyle(this).height));
|
||||
return html`
|
||||
<sl-popup
|
||||
active
|
||||
anchor=${this}
|
||||
auto-size="both"
|
||||
class="hover__popup details hoist details__body"
|
||||
distance=${distance}
|
||||
placement="bottom"
|
||||
sync="width"
|
||||
>
|
||||
${this.tagsTemplate()}
|
||||
</sl-popup>
|
||||
`;
|
||||
}
|
||||
|
||||
tagsTemplate()
|
||||
{
|
||||
return html`${keyed(this._valueUID, map(this.value, (value, index) => this.tagTemplate(value)))}`;
|
||||
@ -1045,6 +1196,21 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
</et2-email-tag>`;
|
||||
}
|
||||
|
||||
protected tagLimitTemplate() : TemplateResult | typeof nothing
|
||||
{
|
||||
if(this._tagsHidden == 0)
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<sl-tag
|
||||
part="tag__limit"
|
||||
class="tag_limit"
|
||||
slot="expand-icon"
|
||||
>+${this._tagsHidden}
|
||||
</sl-tag>`;
|
||||
}
|
||||
|
||||
inputTemplate()
|
||||
{
|
||||
return html`
|
||||
@ -1110,6 +1276,13 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||
const isPlaceholderVisible = this.placeholder && this.value.length === 0 && !this.disabled && !this.readonly;
|
||||
|
||||
let styles = {};
|
||||
|
||||
if(this.rows !== 0)
|
||||
{
|
||||
styles["--height"] = this.rows;
|
||||
}
|
||||
|
||||
// TODO Don't forget required & disabled
|
||||
|
||||
return html`
|
||||
@ -1121,7 +1294,9 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
'form-control--has-label': hasLabel,
|
||||
'form-control--has-help-text': hasHelpText
|
||||
})}
|
||||
style=${styleMap(styles)}
|
||||
@click=${this.handleLabelClick}
|
||||
@mouseenter=${this.handleMouseEnter}
|
||||
@mousedown=${() =>
|
||||
{
|
||||
if(!this.hasFocus)
|
||||
@ -1141,6 +1316,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
<slot name="label">${this.label}</slot>
|
||||
</label>
|
||||
<div part="form-control-input" class="form-control-input">
|
||||
${this.readonlyHoverTemplate()}
|
||||
<sl-popup
|
||||
class=${classMap({
|
||||
email: true,
|
||||
@ -1171,6 +1347,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
||||
<slot part="prefix" name="prefix" class="email__prefix"></slot>
|
||||
${this.tagsTemplate()}
|
||||
${this.inputTemplate()}
|
||||
${this.tagLimitTemplate()}
|
||||
${this.searching ? html`
|
||||
<sl-spinner class="email__loading"></sl-spinner>` : nothing}
|
||||
<slot part="suffix" name="suffix" class="email__suffix"></slot>
|
||||
|
@ -90,7 +90,7 @@ export function inputBasicTests(before : Function, test_value : string, value_se
|
||||
// Shows as empty / no value
|
||||
let value = (<Element><unknown>element).querySelector(value_selector) || (<Element><unknown>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)
|
||||
{
|
||||
@ -104,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><unknown>element);
|
||||
|
||||
|
@ -30,28 +30,24 @@
|
||||
<et2-vbox class="addresses">
|
||||
<et2-hbox>
|
||||
<et2-description value="From" class="firstColumnTitle"></et2-description>
|
||||
<et2-select-email id="additionalfromaddress" readonly="true"
|
||||
fullEmail="@emailTag=fullemail" onlyEmail="@emailTag=onlyemail" multiple="true"
|
||||
onclick="app.mail.onclickCompose"></et2-select-email>
|
||||
<et2-email id="additionalfromaddress" readonly="true"
|
||||
onclick="app.mail.onclickCompose"></et2-email>
|
||||
<et2-date-time align="right" id="date" readonly="true"></et2-date-time>
|
||||
</et2-hbox>
|
||||
<et2-hbox disabled="!@toaddress" width="100%">
|
||||
<et2-description value="To" class="firstColumnTitle"></et2-description>
|
||||
<et2-select-email id="additionaltoaddress" readonly="true" multiple="true"
|
||||
rows="1" fullEmail="@emailTag=fullemail" onlyEmail="@emailTag=onlyemail"
|
||||
onTagClick="app.mail.onclickCompose"></et2-select-email>
|
||||
<et2-email id="additionaltoaddress" readonly="true" rows="1"
|
||||
onTagClick="app.mail.onclickCompose"></et2-email>
|
||||
</et2-hbox>
|
||||
<et2-hbox disabled="!@ccaddress" width="100%">
|
||||
<et2-description value="Cc" class="firstColumnTitle"></et2-description>
|
||||
<et2-select-email id="ccaddress" readonly="true" multiple="true"
|
||||
rows="1" fullEmail="@emailTag=fullemail" onlyEmail="@emailTag=onlyemail"
|
||||
onTagClick="app.mail.onclickCompose"></et2-select-email>
|
||||
<et2-email id="ccaddress" readonly="true" rows="1"
|
||||
onTagClick="app.mail.onclickCompose"></et2-email>
|
||||
</et2-hbox>
|
||||
<et2-hbox disabled="!@bccaddress" width="100%">
|
||||
<et2-description value="Bcc" class="firstColumnTitle"></et2-description>
|
||||
<et2-select-email id="bccaddress" readonly="true" multiple="true"
|
||||
rows="1" fullEmail="@emailTag=fullemail" onlyEmail="@emailTag=onlyemail"
|
||||
onTagClick="app.mail.onclickCompose"></et2-select-email>
|
||||
<et2-email id="bccaddress" readonly="true" rows="1"
|
||||
onTagClick="app.mail.onclickCompose"></et2-email>
|
||||
</et2-hbox>
|
||||
<et2-hbox width="100%" disabled="!@attachmentsBlock">
|
||||
<et2-description value="Attachments" class="firstColumnTitle"></et2-description>
|
||||
|
Loading…
Reference in New Issue
Block a user