From d11be10fa12bd3ca9d4e8c9857a9dc5ff6440527 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 24 Jun 2022 12:10:10 -0600 Subject: [PATCH] Et2SelectEmail: Add button on hover to add a new contact with the email (multiple) --- api/js/etemplate/Et2Select/Et2Select.ts | 31 ++++ api/js/etemplate/Et2Select/Et2SelectEmail.ts | 12 +- api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts | 168 ++++++++++++++++++ api/js/etemplate/etemplate2.ts | 1 + 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts diff --git a/api/js/etemplate/Et2Select/Et2Select.ts b/api/js/etemplate/Et2Select/Et2Select.ts index 11078de833..2f53ab6778 100644 --- a/api/js/etemplate/Et2Select/Et2Select.ts +++ b/api/js/etemplate/Et2Select/Et2Select.ts @@ -24,6 +24,37 @@ export class Et2WidgetWithSelect extends Et2widgetWithSelectMixin(SlSelect) { }; +/** + * Select widget + * + * At its most basic, you can select one option from a list provided. The list can be passed from the server in + * the sel_options array or options can be added as children in the template. Some extending classes provide specific + * options, such as Et2SelectPercent or Et2SelectCountry. All provided options will be mixed together and used. + * + * To allow selecting more than one option, use the attribute multiple="true". This will take & return an array + * as value instead of just a string. + * + * SearchMixin adds additional abilities to ALL select boxes + * @see Et2WithSearchMixin + * + * Override for extending widgets: + * # Custom display of selected value + * When selecting a single value (!multiple) you can override doLabelChange() to customise the displayed label + * @see Et2SelectCategory, which adds in the category icon + * + * # Custom option rows + * Options can have 'class' and 'icon' properties that will be used for the option + * The easiest way for further customisation to use CSS in an external file (like etemplate2.css) and ::part(). + * @see Et2SelectCountry which displays flags via CSS instead of using SelectOption.icon + * + * # Custom tags + * When multiple is set, instead of a single value each selected value is shown in a tag. While it's possible to + * use CSS to some degree, we can also use a custom tag class that extends Et2Tag. + * 1. Create the extending class + * 2. Make sure it's loaded (add to etemplate2.ts) + * 3. In your extending Et2Select, override get tagTag() to return the custom tag name + * + */ // @ts-ignore SlSelect styles is a single CSSResult, not an array, so TS complains export class Et2Select extends Et2WithSearchMixin(Et2InvokerMixin(Et2WidgetWithSelect)) { diff --git a/api/js/etemplate/Et2Select/Et2SelectEmail.ts b/api/js/etemplate/Et2Select/Et2SelectEmail.ts index db0c392585..7b70dee9e0 100644 --- a/api/js/etemplate/Et2Select/Et2SelectEmail.ts +++ b/api/js/etemplate/Et2Select/Et2SelectEmail.ts @@ -141,6 +141,16 @@ export class Et2SelectEmail extends Et2Select super.processRemoteResults(results); } + /** + * Use a custom tag for when multiple=true + * + * @returns {string} + */ + get tagTag() : string + { + return "et2-email-tag"; + } + /** * override tag creation in order to add DND functionality * @param item @@ -149,7 +159,7 @@ export class Et2SelectEmail extends Et2Select protected _createTagNode(item) { let tag = super._createTagNode(item); - if (!this.readonly && this.allowFreeEntries && this.allowDragAndDrop) + if(!this.readonly && this.allowFreeEntries && this.allowDragAndDrop) { let dragTranslate = {x:0,y:0}; tag.class = item.classList.value + " et2-select-draggable"; diff --git a/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts b/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts new file mode 100644 index 0000000000..c51b7370bb --- /dev/null +++ b/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts @@ -0,0 +1,168 @@ +/** + * EGroupware eTemplate2 - Email 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 {css} from "@lion/core"; +import shoelace from "../../Styles/shoelace"; +import {Et2Tag} from "./Et2Tag"; +import {cssImage} from "../../Et2Widget/Et2Widget"; + +/** + * Display a single email address + * On hover, queries the server to see if + * Tag is usually used in a Et2EmailSelect with multiple=true, but there's no reason it can't go anywhere + */ +export class Et2EmailTag extends Et2Tag +{ + private static email_cache : string[] = []; + + static get styles() + { + return [ + super.styles, + shoelace, css` + .tag { + position: relative; + } + .tag__prefix { + display: none; + + height: 16px; + + background-color: white; + background-repeat: no-repeat; + background-size: contain; + } + + .contact_plus .tag__prefix { + display: block; + order: 2; + } + .tag__prefix.loading { + width: 16px; + background-image: ${cssImage("loading")}; + } + + .tag__prefix.contact_plus_add { + width: 16px; + background-image: ${cssImage("add")}; + cursor: pointer; + } + `]; + } + + static get properties() + { + return { + ...super.properties, + /** + * Check if the email is associated with an existing contact, and if it is not show a button to create + * a new contact with this email address. + */ + contact_plus: { + type: Boolean, + reflect: true, + } + } + } + + constructor(...args : []) + { + super(...args); + this.contact_plus = true; + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleMouseLeave = this.handleMouseLeave.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + connectedCallback() + { + super.connectedCallback(); + + if(this.contact_plus && this.egw().app('addressbook')) + { + this.addEventListener("mouseenter", this.handleMouseEnter); + this.addEventListener("mouseleave", this.handleMouseLeave); + } + } + + disconnectedCallback() + { + super.disconnectedCallback(); + this.removeEventListener("mouseenter", this.handleMouseEnter); + this.removeEventListener("mouseleave", this.handleMouseLeave); + } + + public checkContact(email : string) : Promise + { + if(typeof Et2EmailTag.email_cache[email] !== "undefined") + { + return Promise.resolve(Et2EmailTag.email_cache[email]); + } + return this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Url::ajax_contact', [email]).then( + (result) => + { + Et2EmailTag.email_cache[email] = result; + return result; + }); + } + + handleMouseEnter(e : MouseEvent) + { + this.shadowRoot.querySelector(".tag").classList.add("contact_plus"); + this._contactPlusNode.classList.add("loading"); + this._contactPlusNode.style.right = getComputedStyle(this).left; + + this.checkContact(this.value).then((result) => + { + this._contactPlusNode.classList.remove("loading"); + this.handleContactResponse(result); + }) + } + + handleMouseLeave(e : MouseEvent) + { + this.shadowRoot.querySelector(".tag").classList.remove("contact_plus"); + } + + /** + * We either have a contact ID, or false. If false, show the add button. + * @param {boolean | number} data + */ + handleContactResponse(data : boolean | number) + { + if(data) + { + return; + } + this._contactPlusNode.classList.add("contact_plus_add"); + this._contactPlusNode.addEventListener("click", this.handleClick); + } + + handleClick(e : MouseEvent) + { + e.stopPropagation(); + + let extra = { + 'presets[email]': this.value + }; + + this.egw().open('', 'addressbook', 'add', extra); + } + + /** + * Get the node that is shown & clicked on to add email as contact + * + * @returns {Element} + */ + get _contactPlusNode() : HTMLElement + { + return this.shadowRoot.querySelector(".tag__prefix"); + } +} + +customElements.define("et2-email-tag", Et2EmailTag); \ No newline at end of file diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 65c4a67214..8e0893f709 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -58,6 +58,7 @@ import './Et2Select/Et2SelectReadonly'; import './Et2Select/Et2SelectThumbnail' import './Et2Select/Tag/Et2Tag'; import './Et2Select/Tag/Et2CategoryTag'; +import './Et2Select/Tag/Et2EmailTag'; import './Et2Select/Tag/Et2ThumbnailTag'; import './Et2Textarea/Et2Textarea'; import './Et2Textbox/Et2Textbox';