mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 02:43:22 +01:00
Et2Email WIP - Drag & Drop & sort
This commit is contained in:
parent
a60844d45a
commit
e28d38898b
@ -49,12 +49,13 @@ export default css`
|
|||||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-input-focus-ring-color);
|
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-input-focus-ring-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tags */
|
.email .email__prefix {
|
||||||
|
order: 1;
|
||||||
.email .email__combobox > div {
|
|
||||||
margin: auto 0px;
|
|
||||||
}
|
}
|
||||||
|
/* Tags */
|
||||||
.email et2-email-tag {
|
.email et2-email-tag {
|
||||||
|
order: 2;
|
||||||
|
margin: auto 0px;
|
||||||
--icon-width: 1.8em;
|
--icon-width: 1.8em;
|
||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -64,6 +65,7 @@ export default css`
|
|||||||
|
|
||||||
.email__search {
|
.email__search {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
order: 10;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -74,6 +76,9 @@ export default css`
|
|||||||
padding-inline: var(--sl-input-spacing-medium);
|
padding-inline: var(--sl-input-spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email .email__suffix {
|
||||||
|
order: 20;
|
||||||
|
}
|
||||||
/* Listbox */
|
/* Listbox */
|
||||||
|
|
||||||
.email__listbox {
|
.email__listbox {
|
||||||
|
@ -11,6 +11,9 @@ import {html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
|||||||
import {property} from "lit/decorators/property.js";
|
import {property} from "lit/decorators/property.js";
|
||||||
import {state} from "lit/decorators/state.js";
|
import {state} from "lit/decorators/state.js";
|
||||||
import {classMap} from "lit/directives/class-map.js";
|
import {classMap} from "lit/directives/class-map.js";
|
||||||
|
import {keyed} from "lit/directives/keyed.js";
|
||||||
|
import {live} from "lit/directives/live.js";
|
||||||
|
import {map} from "lit/directives/map.js";
|
||||||
import {repeat} from "lit/directives/repeat.js";
|
import {repeat} from "lit/directives/repeat.js";
|
||||||
import {HasSlotController} from "../Et2Widget/slot";
|
import {HasSlotController} from "../Et2Widget/slot";
|
||||||
import {SlOption, SlPopup, SlRemoveEvent} from "@shoelace-style/shoelace";
|
import {SlOption, SlPopup, SlRemoveEvent} from "@shoelace-style/shoelace";
|
||||||
@ -34,12 +37,12 @@ import Sortable from "sortablejs/modular/sortable.complete.esm.js";
|
|||||||
* @dependency et2-email-tag
|
* @dependency et2-email-tag
|
||||||
* @dependency et2-textbox
|
* @dependency et2-textbox
|
||||||
*
|
*
|
||||||
* @slot - The suggestion options. Must be `<sl-option>` elements. You can use `<sl-divider>` to group items visually.
|
|
||||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||||
* @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
|
* @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
|
||||||
|
* @slot suffix - Like prefix, but after
|
||||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||||
*
|
*
|
||||||
* @event sl-change - Emitted when the control's value changes.
|
* @event change - Emitted when the control's value changes.
|
||||||
* @event sl-input - Emitted when the control receives input.
|
* @event sl-input - Emitted when the control receives input.
|
||||||
* @event sl-focus - Emitted when the control gains focus.
|
* @event sl-focus - Emitted when the control gains focus.
|
||||||
* @event sl-blur - Emitted when the control loses focus.
|
* @event sl-blur - Emitted when the control loses focus.
|
||||||
@ -47,20 +50,18 @@ import Sortable from "sortablejs/modular/sortable.complete.esm.js";
|
|||||||
* @event sl-after-show - Emitted after the suggestion menu opens and all animations are complete.
|
* @event sl-after-show - Emitted after the suggestion menu opens and all animations are complete.
|
||||||
* @event sl-hide - Emitted when the suggestion menu closes.
|
* @event sl-hide - Emitted when the suggestion menu closes.
|
||||||
* @event sl-after-hide - Emitted after the suggestion menu closes and all animations are complete.
|
* @event sl-after-hide - Emitted after the suggestion menu closes and all animations are complete.
|
||||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
|
||||||
*
|
*
|
||||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||||
* @csspart form-control-label - The label's wrapper.
|
* @csspart form-control-label - The label's wrapper.
|
||||||
* @csspart form-control-input - The textbox's wrapper.
|
* @csspart form-control-input - The textbox's wrapper.
|
||||||
* @csspart form-control-help-text - The help text's wrapper.
|
* @csspart form-control-help-text - The help text's wrapper.
|
||||||
|
* @csspart combobox - The visible part of the control that is not the listbox - tags, input, prefix & suffix
|
||||||
* @csspart prefix - The container that wraps the prefix slot.
|
* @csspart prefix - The container that wraps the prefix slot.
|
||||||
* @csspart listbox - The listbox container where options are slotted.
|
* @csspart suffix - The container that wraps the suffix slot.
|
||||||
* @csspart tags - The container that houses email tags
|
* @csspart listbox - The listbox container where suggestions are slotted.
|
||||||
|
* @csspart input - The input element
|
||||||
|
* @csspart option - Each matching email address suggestion
|
||||||
* @csspart tag - The individual tags that represent each email address.
|
* @csspart tag - The individual tags that represent each email address.
|
||||||
* @csspart tag__base - The tag's base part.
|
|
||||||
* @csspart tag__content - The tag's content part.
|
|
||||||
* @csspart tag__remove-button - The tag's remove button.
|
|
||||||
* @csspart tag__remove-button__base - The tag's remove button base part.
|
|
||||||
*/
|
*/
|
||||||
export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinInterface
|
export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinInterface
|
||||||
{
|
{
|
||||||
@ -76,7 +77,8 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current value of the component, an array of valid email addresses
|
* The current value of the component, an array of valid email addresses.
|
||||||
|
* If allowPlaceholder=true, placeholders are also allowed
|
||||||
*/
|
*/
|
||||||
@property({
|
@property({
|
||||||
converter: {
|
converter: {
|
||||||
@ -108,7 +110,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
/** Allow drag and drop tags between two or more Et2Email widgets */
|
/** Allow drag and drop tags between two or more Et2Email widgets */
|
||||||
@property({type: Boolean})
|
@property({type: Boolean})
|
||||||
allowDragAndDrop? : boolean;
|
allowDragAndDrop? : boolean = true;
|
||||||
|
|
||||||
/** Allow placeholders like {{email}}, as well as real email-addresses */
|
/** Allow placeholders like {{email}}, as well as real email-addresses */
|
||||||
@property({type: Boolean})
|
@property({type: Boolean})
|
||||||
@ -162,26 +164,35 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
* When user is typing, we wait this long for them to be finished before we start the search
|
* When user is typing, we wait this long for them to be finished before we start the search
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @protected
|
* @protected
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
public static SEARCH_TIMEOUT = 500;
|
public static SEARCH_TIMEOUT : number = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typing these characters will end the email address and start a new one
|
* Typing these characters will end the email address and start a new one
|
||||||
* @type {string[]}
|
* @type {string[]}
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
public static TAG_BREAK : string[] = ["Tab", "Enter", ","];
|
public static TAG_BREAK : string[] = ["Tab", "Enter", ","];
|
||||||
|
|
||||||
protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||||
|
|
||||||
/** User preference to immediately close the search results after selecting a match */
|
/** User preference to immediately close the search results after selecting a match
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
protected _close_on_select = true;
|
protected _close_on_select = true;
|
||||||
|
|
||||||
protected _searchTimeout : number;
|
protected _searchTimeout : number;
|
||||||
protected _searchPromise : Promise<SelectOption[]> = Promise.resolve([]);
|
protected _searchPromise : Promise<SelectOption[]> = Promise.resolve([]);
|
||||||
protected _selectOptions : SelectOption[] = [];
|
protected _selectOptions : SelectOption[] = [];
|
||||||
|
|
||||||
|
// Drag / drop / sort
|
||||||
protected _sortable : Sortable;
|
protected _sortable : Sortable;
|
||||||
|
|
||||||
|
// UID to force Lit to re-draw tags after sort
|
||||||
|
private _valueUID : string;
|
||||||
|
|
||||||
constructor(...args : any[])
|
constructor(...args : any[])
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -194,12 +205,26 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
this.handleOpenChange = this.handleOpenChange.bind(this);
|
this.handleOpenChange = this.handleOpenChange.bind(this);
|
||||||
this.handleLostFocus = this.handleLostFocus.bind(this);
|
this.handleLostFocus = this.handleLostFocus.bind(this);
|
||||||
|
|
||||||
|
this.handleSortEnd = this.handleSortEnd.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback()
|
connectedCallback()
|
||||||
{
|
{
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
this._valueUID = this.egw().uid();
|
||||||
|
this.updateComplete.then(() => this.makeSortable());
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback()
|
||||||
|
{
|
||||||
|
super.disconnectedCallback();
|
||||||
|
|
||||||
|
if(this._sortable)
|
||||||
|
{
|
||||||
|
this._sortable.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
willUpdate(changedProperties : PropertyValues)
|
willUpdate(changedProperties : PropertyValues)
|
||||||
@ -248,7 +273,33 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
protected makeSortable()
|
protected makeSortable()
|
||||||
{
|
{
|
||||||
// TODO
|
if(this._sortable)
|
||||||
|
{
|
||||||
|
this._sortable.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.allowDragAndDrop)
|
||||||
|
{
|
||||||
|
this.classList.remove("et2-sortable-email");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(this, " is sortable");
|
||||||
|
this.classList.add("et2-sortable-email");
|
||||||
|
let pull : boolean | string = !this.disabled && !this.readonly;
|
||||||
|
if(this.readonly && !this.disabled)
|
||||||
|
{
|
||||||
|
pull = 'clone';
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sortable = Sortable.create(this.shadowRoot.querySelector('.email__combobox'), {
|
||||||
|
draggable: "et2-email-tag",
|
||||||
|
group: {
|
||||||
|
name: "email",
|
||||||
|
pull: pull,
|
||||||
|
put: !(this.readonly || this.disabled)
|
||||||
|
},
|
||||||
|
onEnd: this.handleSortEnd
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -296,7 +347,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an entry that is not in the options and add it to the value
|
* Create an entry that is not in the suggestions and add it to the value
|
||||||
*
|
*
|
||||||
* @param {string} text Used as both value and label
|
* @param {string} text Used as both value and label
|
||||||
*/
|
*/
|
||||||
@ -391,10 +442,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start searching
|
* Start searching for contacts matching what has been typed
|
||||||
*
|
|
||||||
* If we have local options, we'll search & display any matches.
|
|
||||||
* If serverUrl is set, we'll ask the server for results as well.
|
|
||||||
*/
|
*/
|
||||||
public async startSearch()
|
public async startSearch()
|
||||||
{
|
{
|
||||||
@ -431,6 +479,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @returns Promise<SelectOption[]>
|
* @returns Promise<SelectOption[]>
|
||||||
* @protected
|
* @protected
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected remoteSearch(search : string, options : object) : Promise<SelectOption[]>
|
protected remoteSearch(search : string, options : object) : Promise<SelectOption[]>
|
||||||
{
|
{
|
||||||
@ -453,6 +502,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
*
|
*
|
||||||
* @param results
|
* @param results
|
||||||
* @protected
|
* @protected
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected processRemoteResults(entries)
|
protected processRemoteResults(entries)
|
||||||
{
|
{
|
||||||
@ -467,6 +517,66 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end of a sort, either internal or between widgets that deal with email
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @protected
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected handleSortEnd(event)
|
||||||
|
{
|
||||||
|
if(this.disabled || this.readonly || !event.item?.value || !this.validateAddress(event.item.value) ||
|
||||||
|
// No real change
|
||||||
|
event.from === event.to && event.oldDraggableIndex == event.newDraggableIndex
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = <Et2EmailTag>event.item;
|
||||||
|
const from = Sortable.utils.closest(event.from, "et2-email, .et2-sortable-email");
|
||||||
|
const to = Sortable.utils.closest(event.to, "et2-email, .et2-sortable-email");
|
||||||
|
|
||||||
|
if(from == this)
|
||||||
|
{
|
||||||
|
const index = this.value.indexOf(tag.value);
|
||||||
|
|
||||||
|
if(index > -1)
|
||||||
|
{
|
||||||
|
this.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset focus
|
||||||
|
/*
|
||||||
|
if(typeof from.focus == "function")
|
||||||
|
{
|
||||||
|
this.updateComplete.then(() =>
|
||||||
|
{
|
||||||
|
from.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Update key to force Lit to redraw tags
|
||||||
|
this._valueUID = this.egw()?.uid() ?? new Date().toISOString();
|
||||||
|
}
|
||||||
|
if(to === this)
|
||||||
|
{
|
||||||
|
let targetIndex = typeof event.newDraggableIndex == "number" ? event.newDraggableIndex : this.value.length;
|
||||||
|
this.value.splice(targetIndex, 0, tag.value);
|
||||||
|
|
||||||
|
// Update key to force Lit to redraw tags
|
||||||
|
this._valueUID = this.egw()?.uid() ?? new Date().toISOString();
|
||||||
|
}
|
||||||
|
else if(typeof to.handleSortEnd == "function")
|
||||||
|
{
|
||||||
|
to.handleSortEnd(event);
|
||||||
|
}
|
||||||
|
// Remove tag to avoid occasional duplication
|
||||||
|
tag.remove();
|
||||||
|
|
||||||
|
this.requestUpdate("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focus has gone somewhere else
|
* Focus has gone somewhere else
|
||||||
@ -474,6 +584,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
*/
|
*/
|
||||||
private handleLostFocus = (event : MouseEvent | KeyboardEvent) =>
|
private handleLostFocus = (event : MouseEvent | KeyboardEvent) =>
|
||||||
{
|
{
|
||||||
|
console.log(this, "lost focus");
|
||||||
// Close when clicking outside of the component
|
// Close when clicking outside of the component
|
||||||
const path = event.composedPath();
|
const path = event.composedPath();
|
||||||
if(this && !path.includes(this))
|
if(this && !path.includes(this))
|
||||||
@ -797,14 +908,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
tagsTemplate()
|
tagsTemplate()
|
||||||
{
|
{
|
||||||
return this.value.map((value, index) =>
|
return html`${keyed(this._valueUID, map(this.value, (value, index) => this.tagTemplate(value)))}`;
|
||||||
{
|
|
||||||
// Wrap so we can handle the remove
|
|
||||||
return html`
|
|
||||||
<div @sl-remove=${(e : SlRemoveEvent) => this.handleTagRemove(e, value)}>
|
|
||||||
${this.tagTemplate(value)}
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tagTemplate(value)
|
tagTemplate(value)
|
||||||
@ -814,15 +918,17 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<et2-email-tag
|
<et2-email-tag
|
||||||
|
part="tag"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"et2-select-draggable": !this.readonly && this.allowDragAndDrop,
|
"et2-select-draggable": !this.readonly && this.allowDragAndDrop,
|
||||||
})}
|
})}
|
||||||
.fullEmail=${this.fullEmail}
|
.fullEmail=${this.fullEmail}
|
||||||
.onlyEmail=${this.onlyEmail}
|
.onlyEmail=${this.onlyEmail}
|
||||||
.value=${value}
|
.value=${live(value)}
|
||||||
?removable=${!readonly}
|
?removable=${!readonly}
|
||||||
?readonly=${readonly}
|
?readonly=${readonly}
|
||||||
?editable=${isEditable}
|
?editable=${isEditable}
|
||||||
|
@sl-remove=${(e : SlRemoveEvent) => this.handleTagRemove(e, value)}
|
||||||
@mousedown=${(e) => {this._cancelOpen = true;}}
|
@mousedown=${(e) => {this._cancelOpen = true;}}
|
||||||
@dblclick=${(e) => {e.target.startEdit();}}
|
@dblclick=${(e) => {e.target.startEdit();}}
|
||||||
@change=${this.handleTagChange}
|
@change=${this.handleTagChange}
|
||||||
@ -904,6 +1010,14 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
|
|||||||
'form-control--has-help-text': hasHelpText
|
'form-control--has-help-text': hasHelpText
|
||||||
})}
|
})}
|
||||||
@click=${this.handleLabelClick}
|
@click=${this.handleLabelClick}
|
||||||
|
@mousedown=${() =>
|
||||||
|
{
|
||||||
|
if(!this.hasFocus)
|
||||||
|
{
|
||||||
|
// Helps Sortable work every time
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
id="label"
|
id="label"
|
||||||
|
Loading…
Reference in New Issue
Block a user