mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-23 07:09:20 +01:00
Et2Select: Implement allowFreeEntries & editModeEnabled properties
Also added Et2SelectEmail, which uses them
This commit is contained in:
parent
d98978ddd3
commit
531cc473e2
132
api/js/etemplate/Et2Select/Et2SelectEmail.ts
Normal file
132
api/js/etemplate/Et2Select/Et2SelectEmail.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* EGroupware eTemplate2 - Email-selection WebComponent
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
* @package api
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Et2Select} from "./Et2Select";
|
||||||
|
import {css, html} from "@lion/core";
|
||||||
|
import {IsEmail} from "../Validators/IsEmail";
|
||||||
|
|
||||||
|
export class Et2SelectEmail extends Et2Select
|
||||||
|
{
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
::part(icon), .select__icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
::slotted(sl-icon[slot="suffix"]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(...args : any[])
|
||||||
|
{
|
||||||
|
super(...args);
|
||||||
|
this.search = true;
|
||||||
|
this.searchUrl = "EGroupware\\Api\\Etemplate\\Widget\\Taglist::ajax_email";
|
||||||
|
this.allowFreeEntries = true;
|
||||||
|
this.editModeEnabled = true;
|
||||||
|
this.multiple = true;
|
||||||
|
this.defaultValidators.push(new IsEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback()
|
||||||
|
{
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually query the server.
|
||||||
|
*
|
||||||
|
* Overridden to change request to match server
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @param {object} options
|
||||||
|
* @returns {any}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected remoteQuery(search : string, options : object)
|
||||||
|
{
|
||||||
|
return this.egw().json(this.searchUrl, [search]).sendRequest().then((result) =>
|
||||||
|
{
|
||||||
|
this.processRemoteResults(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add in remote results
|
||||||
|
*
|
||||||
|
* Overridden to get results in a format parent expects.
|
||||||
|
* Current server-side gives {
|
||||||
|
* icon: "/egroupware/api/avatar.php?contact_id=5&etag=1"
|
||||||
|
* id: "ng@egroupware.org"
|
||||||
|
* label: "ng@egroupware.org"
|
||||||
|
* name: ""
|
||||||
|
* title: "ng@egroupware.org"
|
||||||
|
* }
|
||||||
|
* Parent expects value instead of id
|
||||||
|
*
|
||||||
|
* @param results
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected processRemoteResults(results)
|
||||||
|
{
|
||||||
|
results.forEach(r => r.value = r.id);
|
||||||
|
super.processRemoteResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customise how tags are rendered. This overrides what SlSelect
|
||||||
|
* does in syncItemsFromValue().
|
||||||
|
* This is a copy+paste from SlSelect.syncItemsFromValue().
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _tagTemplate(item)
|
||||||
|
{
|
||||||
|
let image = item.querySelector("et2-image");
|
||||||
|
if(image)
|
||||||
|
{
|
||||||
|
image = image.clone();
|
||||||
|
image.slot = "prefix";
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<et2-tag
|
||||||
|
removable
|
||||||
|
@click=${this.handleTagInteraction}
|
||||||
|
@keydown=${this.handleTagInteraction}
|
||||||
|
@sl-remove=${(event) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
if(!this.disabled)
|
||||||
|
{
|
||||||
|
item.checked = false;
|
||||||
|
this.syncValueFromItems();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${image}
|
||||||
|
${this.getItemLabel(item)}
|
||||||
|
</et2-tag>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore TypeScript is not recognizing that this widget is a LitElement
|
||||||
|
customElements.define("et2-select-email", Et2SelectEmail);
|
@ -8,9 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import {css, dedupeMixin, html, LitElement, render, repeat, SlotMixin} from "@lion/core";
|
import {css, html, LitElement, render, repeat, SlotMixin} from "@lion/core";
|
||||||
import {cleanSelectOptions, SelectOption} from "./FindSelectOptions";
|
import {cleanSelectOptions, SelectOption} from "./FindSelectOptions";
|
||||||
|
import {Validator} from "@lion/form-core";
|
||||||
|
import {Et2Tag} from "./Tag/Et2Tag";
|
||||||
|
|
||||||
|
// Otherwise import gets stripped
|
||||||
|
let keep_import : Et2Tag;
|
||||||
|
|
||||||
// Export the Interface for TypeScript
|
// Export the Interface for TypeScript
|
||||||
type Constructor<T = {}> = new (...args : any[]) => T;
|
type Constructor<T = {}> = new (...args : any[]) => T;
|
||||||
@ -67,7 +71,7 @@ export declare class SearchMixinInterface
|
|||||||
*
|
*
|
||||||
* Currently I assume we're extending an Et2Select, so changes may need to be made for better abstraction
|
* Currently I assume we're extending an Et2Select, so changes may need to be made for better abstraction
|
||||||
*/
|
*/
|
||||||
export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass : T) =>
|
||||||
{
|
{
|
||||||
class Et2WidgetWithSearch extends SlotMixin(superclass)
|
class Et2WidgetWithSearch extends SlotMixin(superclass)
|
||||||
{
|
{
|
||||||
@ -79,9 +83,18 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
|
|
||||||
searchUrl: {type: String},
|
searchUrl: {type: String},
|
||||||
|
|
||||||
allowFreeEntries: {type: Boolean},
|
/**
|
||||||
|
* Allow custom entries that are not in the options
|
||||||
|
*/
|
||||||
|
allowFreeEntries: {type: Boolean, reflect: true},
|
||||||
|
|
||||||
searchOptions: {type: Object}
|
searchOptions: {type: Object},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow editing tags by clicking on them.
|
||||||
|
* allowFreeEntries must be true
|
||||||
|
*/
|
||||||
|
editModeEnabled: {type: Boolean}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,12 +117,18 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
...(super.styles ? (Symbol.iterator in Object(super.styles) ? super.styles : [super.styles]) : []),
|
...(super.styles ? (Symbol.iterator in Object(super.styles) ? super.styles : [super.styles]) : []),
|
||||||
css`
|
css`
|
||||||
|
/* Show / hide SlSelect icons - dropdown arrow, etc */
|
||||||
::slotted([slot="suffix"]) {
|
::slotted([slot="suffix"]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
:host([search]) ::slotted([slot="suffix"]) {
|
:host([search]) ::slotted([slot="suffix"]) {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
:host([allowFreeEntries]) ::slotted([slot="suffix"]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make textbox take full width */
|
||||||
::slotted([name="search_input"]:focus ){
|
::slotted([name="search_input"]:focus ){
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -117,21 +136,35 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
flex: 2 1 auto;
|
flex: 2 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search textbox general styling, starts hidden */
|
||||||
.select__prefix ::slotted(.search_input) {
|
.select__prefix ::slotted(.search_input) {
|
||||||
display: none;
|
display: none;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
/* Search UI active - show textbox & stuff */
|
||||||
::slotted(.search_input.active) {
|
::slotted(.search_input.active) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide options that do not match current search text */
|
||||||
::slotted(.no-match) {
|
::slotted(.no-match) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep overflow tag right-aligned. It's the only sl-tag. */
|
||||||
|
.select__tags sl-tag {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Borrowed from Lion ValidatorMixin, but we don't want the whole thing
|
||||||
|
protected defaultValidators : Validator[];
|
||||||
|
protected validators : Validator[];
|
||||||
|
|
||||||
private _searchTimeout : number;
|
private _searchTimeout : number;
|
||||||
protected static SEARCH_TIMEOUT = 500;
|
protected static SEARCH_TIMEOUT = 500;
|
||||||
protected static MIN_CHARS = 2;
|
protected static MIN_CHARS = 2;
|
||||||
@ -144,9 +177,25 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
this.searchUrl = "";
|
this.searchUrl = "";
|
||||||
this.searchOptions = {};
|
this.searchOptions = {};
|
||||||
|
|
||||||
|
this.allowFreeEntries = false;
|
||||||
|
this.editModeEnabled = false;
|
||||||
|
|
||||||
|
this.validators = [];
|
||||||
|
/**
|
||||||
|
* Used by Subclassers to add default Validators.
|
||||||
|
* A email input for instance, always needs the isEmail validator.
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* this.defaultValidators.push(new IsDate());
|
||||||
|
* ```
|
||||||
|
* @type {Validator[]}
|
||||||
|
*/
|
||||||
|
this.defaultValidators = [];
|
||||||
|
|
||||||
this._handleSearchButtonClick = this._handleSearchButtonClick.bind(this);
|
this._handleSearchButtonClick = this._handleSearchButtonClick.bind(this);
|
||||||
this._handleSearchAbort = this._handleSearchAbort.bind(this);
|
this._handleSearchAbort = this._handleSearchAbort.bind(this);
|
||||||
this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this);
|
this._handleSearchKeyDown = this._handleSearchKeyDown.bind(this);
|
||||||
|
this.handleTagInteraction = this.handleTagInteraction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback()
|
connectedCallback()
|
||||||
@ -241,6 +290,35 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
return this.querySelectorAll("sl-menu-item.remote");
|
return this.querySelectorAll("sl-menu-item.remote");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get value()
|
||||||
|
{
|
||||||
|
return super.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(new_value : string | string[])
|
||||||
|
{
|
||||||
|
super.value = new_value;
|
||||||
|
|
||||||
|
// Overridden to add options if allowFreeEntries=true
|
||||||
|
if(this.allowFreeEntries)
|
||||||
|
{
|
||||||
|
if(typeof this.value == "string" && !this.select_options.find(o => o.value == value))
|
||||||
|
{
|
||||||
|
this.createFreeEntry(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.value.forEach((e) =>
|
||||||
|
{
|
||||||
|
if(!this.select_options.find(o => o.value == e))
|
||||||
|
{
|
||||||
|
this.createFreeEntry(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getItems()
|
getItems()
|
||||||
{
|
{
|
||||||
return [...this.querySelectorAll("sl-menu-item:not(.no-match)")];
|
return [...this.querySelectorAll("sl-menu-item:not(.no-match)")];
|
||||||
@ -295,6 +373,33 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTagInteraction(event : KeyboardEvent | MouseEvent)
|
||||||
|
{
|
||||||
|
let result = super.handleTagInteraction(event);
|
||||||
|
|
||||||
|
// Check if remove button was clicked
|
||||||
|
const path = event.composedPath();
|
||||||
|
const clearButton = path.find((el) =>
|
||||||
|
{
|
||||||
|
if(el instanceof HTMLElement)
|
||||||
|
{
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
return element.classList.contains('tag__remove');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// No edit, or removed tag
|
||||||
|
if(!this.editModeEnabled || clearButton)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the tag
|
||||||
|
const tag = <Et2Tag>path.find((el) => el instanceof Et2Tag);
|
||||||
|
this.startEdit(tag);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value was cleared
|
* Value was cleared
|
||||||
*/
|
*/
|
||||||
@ -325,8 +430,19 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
if(event.key === "Enter")
|
if(event.key === "Enter")
|
||||||
{
|
{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if(this.allowFreeEntries && this.createFreeEntry(this._searchInputNode.value))
|
||||||
|
{
|
||||||
|
this._searchInputNode.value = "";
|
||||||
|
if(!this.multiple)
|
||||||
|
{
|
||||||
|
this.dropdown.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this.startSearch();
|
this.startSearch();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start the search automatically if they have enough letters
|
// Start the search automatically if they have enough letters
|
||||||
clearTimeout(this._searchTimeout);
|
clearTimeout(this._searchTimeout);
|
||||||
@ -403,9 +519,19 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually query the server.
|
||||||
|
*
|
||||||
|
* This can be overridden to change request parameters
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @param {object} options
|
||||||
|
* @returns {any}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
protected remoteQuery(search : string, options : object)
|
protected remoteQuery(search : string, options : object)
|
||||||
{
|
{
|
||||||
return this.egw().request(this.searchUrl, [search]).sendRequest().then((result) =>
|
return this.egw().request(this.searchUrl, [search]).then((result) =>
|
||||||
{
|
{
|
||||||
this.processRemoteResults(result);
|
this.processRemoteResults(result);
|
||||||
});
|
});
|
||||||
@ -460,11 +586,146 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
return item.value == search;
|
return item.value == search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an entry that is not in the options and add it to the value
|
||||||
|
*
|
||||||
|
* @param {string} text Used as both value and label
|
||||||
|
*/
|
||||||
|
public createFreeEntry(text : string) : boolean
|
||||||
|
{
|
||||||
|
if(!this.validateFreeEntry(text))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Make sure not to double-add
|
||||||
|
if(!this.select_options.find(o => o.value == text))
|
||||||
|
{
|
||||||
|
this.select_options.push(<SelectOption>{
|
||||||
|
value: text,
|
||||||
|
label: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Make sure not to double-add
|
||||||
|
if(this.multiple && this.value.indexOf(text) == -1)
|
||||||
|
{
|
||||||
|
this.value.push(text);
|
||||||
|
}
|
||||||
|
else if(!this.multiple)
|
||||||
|
{
|
||||||
|
this.value = text;
|
||||||
|
}
|
||||||
|
this.requestUpdate('select_options');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a free entry value is acceptable.
|
||||||
|
* We use validators directly using the proposed value
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public validateFreeEntry(text) : boolean
|
||||||
|
{
|
||||||
|
let validators = [...this.validators, ...this.defaultValidators];
|
||||||
|
let result = validators.filter(v =>
|
||||||
|
v.execute(text, v.param, {node: this}),
|
||||||
|
);
|
||||||
|
return result.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public startEdit(tag : Et2Tag)
|
||||||
|
{
|
||||||
|
// Turn on edit UI
|
||||||
|
this.handleMenuShow();
|
||||||
|
|
||||||
|
// but hide the menu
|
||||||
|
this.updateComplete.then(() => this.dropdown.hide());
|
||||||
|
|
||||||
|
// Pre-set value to tag value
|
||||||
|
this._searchInputNode.value = tag.textContent.trim();
|
||||||
|
|
||||||
|
// Remove from value & DOM. If they finish the edit, the new one will be added.
|
||||||
|
this.value = this.value.filter(v => v !== this._searchInputNode.value);
|
||||||
|
tag.remove();
|
||||||
|
}
|
||||||
|
|
||||||
protected _handleSearchButtonClick(e)
|
protected _handleSearchButtonClick(e)
|
||||||
{
|
{
|
||||||
this.handleMenuShow();
|
this.handleMenuShow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method from SlSelect to stick our own tags in there
|
||||||
|
*/
|
||||||
|
syncItemsFromValue()
|
||||||
|
{
|
||||||
|
if(typeof super.syncItemsFromValue === "function")
|
||||||
|
{
|
||||||
|
super.syncItemsFromValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only applies to multiple
|
||||||
|
if(typeof this.displayTags !== "object" || !this.multiple)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let overflow = null;
|
||||||
|
if(this.maxTagsVisible > 0 && this.displayTags.length > this.maxTagsVisible)
|
||||||
|
{
|
||||||
|
overflow = this.displayTags.pop();
|
||||||
|
}
|
||||||
|
const checkedItems = Object.values(this.menuItems).filter(item => this.value.includes(item.value));
|
||||||
|
this.displayTags = checkedItems.map(item => this._tagTemplate(item));
|
||||||
|
|
||||||
|
// Re-slice & add overflow tag
|
||||||
|
if(overflow)
|
||||||
|
{
|
||||||
|
this.displayTags = this.displayTags.slice(0, this.maxTagsVisible);
|
||||||
|
this.displayTags.push(overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customise how tags are rendered. This overrides what SlSelect
|
||||||
|
* does in syncItemsFromValue().
|
||||||
|
* This is a copy+paste from SlSelect.syncItemsFromValue().
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _tagTemplate(item)
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<et2-tag
|
||||||
|
part="tag"
|
||||||
|
exportparts="
|
||||||
|
base:tag__base,
|
||||||
|
content:tag__content,
|
||||||
|
remove-button:tag__remove-button
|
||||||
|
"
|
||||||
|
variant="neutral"
|
||||||
|
size=${this.size}
|
||||||
|
?pill=${this.pill}
|
||||||
|
removable
|
||||||
|
@click=${this.handleTagInteraction}
|
||||||
|
@keydown=${this.handleTagInteraction}
|
||||||
|
@sl-remove=${(event) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
if(!this.disabled)
|
||||||
|
{
|
||||||
|
item.checked = false;
|
||||||
|
this.syncValueFromItems();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${this.getItemLabel(item)}
|
||||||
|
</et2-tag>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected _handleSearchAbort(e)
|
protected _handleSearchAbort(e)
|
||||||
{
|
{
|
||||||
this._activeControls.classList.remove("active");
|
this._activeControls.classList.remove("active");
|
||||||
@ -481,5 +742,5 @@ export const Et2WithSearchMixin = dedupeMixin((superclass) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Et2WidgetWithSearch as Constructor<SearchMixinInterface> & T & LitElement;
|
return Et2WidgetWithSearch as unknown as Constructor<SearchMixinInterface> & T;
|
||||||
});
|
}
|
84
api/js/etemplate/Et2Select/Tag/Et2Tag.ts
Normal file
84
api/js/etemplate/Et2Select/Tag/Et2Tag.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* EGroupware eTemplate2 - Tag WebComponent
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
* @package api
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
*/
|
||||||
|
import {Et2Widget} from "../../Et2Widget/Et2Widget";
|
||||||
|
import {SlTag} from "@shoelace-style/shoelace";
|
||||||
|
import {classMap, css, html} from "@lion/core";
|
||||||
|
import shoelace from "../../Styles/shoelace";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag is usually used in a Select with multiple=true, but there's no reason it can't go anywhere
|
||||||
|
*/
|
||||||
|
export class Et2Tag extends Et2Widget(SlTag)
|
||||||
|
{
|
||||||
|
static get styles()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
super.styles,
|
||||||
|
shoelace, css`
|
||||||
|
::slotted(et2-image)
|
||||||
|
{
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
`];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(...args : [])
|
||||||
|
{
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
return html`
|
||||||
|
<span
|
||||||
|
part="base"
|
||||||
|
class=${classMap({
|
||||||
|
tag: true,
|
||||||
|
// Types
|
||||||
|
'tag--primary': this.variant === 'primary',
|
||||||
|
'tag--success': this.variant === 'success',
|
||||||
|
'tag--neutral': this.variant === 'neutral',
|
||||||
|
'tag--warning': this.variant === 'warning',
|
||||||
|
'tag--danger': this.variant === 'danger',
|
||||||
|
'tag--text': this.variant === 'text',
|
||||||
|
// Sizes
|
||||||
|
'tag--small': this.size === 'small',
|
||||||
|
'tag--medium': this.size === 'medium',
|
||||||
|
'tag--large': this.size === 'large',
|
||||||
|
// Modifiers
|
||||||
|
'tag--pill': this.pill,
|
||||||
|
'tag--removable': this.removable
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span part="prefix" class="tag__prefix">
|
||||||
|
<slot name="prefix"></slot>
|
||||||
|
</span>
|
||||||
|
<span part="content" class="tag__content">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
${this.removable
|
||||||
|
? html`
|
||||||
|
<sl-icon-button
|
||||||
|
part="remove-button"
|
||||||
|
exportparts="base:remove-button__base"
|
||||||
|
name="x"
|
||||||
|
library="system"
|
||||||
|
label=${this.egw().lang('remove')}
|
||||||
|
class="tag__remove"
|
||||||
|
@click=${this.handleRemoveClick}
|
||||||
|
></sl-icon-button>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("et2-tag", Et2Tag);
|
@ -51,7 +51,9 @@ import './Et2Link/Et2LinkString';
|
|||||||
import './Et2Link/Et2LinkTo';
|
import './Et2Link/Et2LinkTo';
|
||||||
import './Et2Select/Et2Select';
|
import './Et2Select/Et2Select';
|
||||||
import './Et2Select/Et2SelectAccount';
|
import './Et2Select/Et2SelectAccount';
|
||||||
|
import './Et2Select/Et2SelectEmail';
|
||||||
import './Et2Select/Et2SelectReadonly';
|
import './Et2Select/Et2SelectReadonly';
|
||||||
|
import './Et2Select/Tag/Et2Tag';
|
||||||
import './Et2Textarea/Et2Textarea';
|
import './Et2Textarea/Et2Textarea';
|
||||||
import './Et2Textbox/Et2Textbox';
|
import './Et2Textbox/Et2Textbox';
|
||||||
import './Et2Textbox/Et2TextboxReadonly';
|
import './Et2Textbox/Et2TextboxReadonly';
|
||||||
|
@ -111,8 +111,9 @@ class Taglist extends Etemplate\Widget
|
|||||||
*
|
*
|
||||||
* Uses the mail application if available, or addressbook
|
* Uses the mail application if available, or addressbook
|
||||||
*/
|
*/
|
||||||
public static function ajax_email()
|
public static function ajax_email($search)
|
||||||
{
|
{
|
||||||
|
$_REQUEST['query'] = $_REQUEST['query'] ?: $search;
|
||||||
// If no mail app access, use link system -> addressbook
|
// If no mail app access, use link system -> addressbook
|
||||||
if(!$GLOBALS['egw_info']['apps']['mail'])
|
if(!$GLOBALS['egw_info']['apps']['mail'])
|
||||||
{
|
{
|
||||||
@ -171,21 +172,24 @@ class Taglist extends Etemplate\Widget
|
|||||||
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]);
|
unset($value[$key]);
|
||||||
}
|
}
|
||||||
if($this->type == 'taglist-email' && $this->attrs['include_lists'] && is_numeric($val))
|
if(str_contains($this->type, 'email') && $this->attrs['include_lists'] && is_numeric($val))
|
||||||
{
|
{
|
||||||
$lists = $GLOBALS['egw']->contacts->get_lists(Api\Acl::READ);
|
$lists = $GLOBALS['egw']->contacts->get_lists(Api\Acl::READ);
|
||||||
if(!array_key_exists($val, $lists))
|
if(!array_key_exists($val, $lists))
|
||||||
{
|
{
|
||||||
self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!",$val,implode("','",array_keys($lists))),'');
|
self::set_validation_error($form_name, lang("'%1' is NOT allowed ('%2')!", $val, implode("','", array_keys($lists))), '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if($this->type == 'taglist-email' && !preg_match(Url::EMAIL_PREG, $val) &&
|
else
|
||||||
!($this->attrs['domainOptional'] && preg_match (Taglist::EMAIL_PREG_NO_DOMAIN, $val)) &&
|
{
|
||||||
|
if(str_contains($this->type, 'email') && !preg_match(Url::EMAIL_PREG, $val) &&
|
||||||
|
!($this->attrs['domainOptional'] && preg_match(Taglist::EMAIL_PREG_NO_DOMAIN, $val)) &&
|
||||||
// Allow merge placeholders. Might be a better way to do this though.
|
// Allow merge placeholders. Might be a better way to do this though.
|
||||||
!preg_match('/{{.+}}|\$\$.+\$\$/',$val)
|
!preg_match('/{{.+}}|\$\$.+\$\$/', $val)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
self::set_validation_error($form_name,lang("'%1' has an invalid format",$val),'');
|
self::set_validation_error($form_name, lang("'%1' has an invalid format", $val), '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($ok && $value === '' && $this->attrs['needed'])
|
if ($ok && $value === '' && $this->attrs['needed'])
|
||||||
@ -204,3 +208,5 @@ class Taglist extends Etemplate\Widget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Taglist', array('taglist', 'et2-select-email'));
|
Loading…
Reference in New Issue
Block a user