From 7f987d9e0f2ca46d335bfa1b8c47799a89662632 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 16 Jan 2024 15:29:12 -0700 Subject: [PATCH] Refactor email address formatting and use it in Et2EmailTag and Et2UrlEmailReadonly --- api/js/etemplate/Et2Email/utils.ts | 166 +++++++++++++++++ api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts | 167 ++++-------------- .../etemplate/Et2Url/Et2UrlEmailReadonly.ts | 69 ++------ 3 files changed, 215 insertions(+), 187 deletions(-) create mode 100644 api/js/etemplate/Et2Email/utils.ts diff --git a/api/js/etemplate/Et2Email/utils.ts b/api/js/etemplate/Et2Email/utils.ts new file mode 100644 index 0000000000..3b8bc46c51 --- /dev/null +++ b/api/js/etemplate/Et2Email/utils.ts @@ -0,0 +1,166 @@ +/** + * Email address UI utilities + * + * You probably want formatEmailAddress(address) + */ + +function _getEmailDisplayPreference() +{ + const pref = window.egw.preference("emailTag", "mail") ?? ""; + switch(pref) + { + case "fullemail": + return "full" + default: + case "onlyname": + return "name"; + case "onlyemail": + return "email"; + case "domain": + return "domain"; + } +} + + +const email_cache : { [address : string] : ContactInfo | false } = {}; + +let contact_request : Promise; +let contact_requests : { [key : string] : Array; } = {}; + +/** + * Cache information about a contact + */ +export interface ContactInfo +{ + id : number, + n_fn : string, + lname? : string, + fname? : string, + photo? : string, + email? : string, + email_home : string +} + +/** + * Get contact information using an email address + * + * @param {string} email + * @returns {Promise} + */ +export function checkContact(email : string) : Promise +{ + if(typeof email_cache[email] !== "undefined") + { + return Promise.resolve(email_cache[email]); + } + if(!contact_request && window.egw) + { + contact_request = window.egw.jsonq('EGroupware\\Api\\Etemplate\\Widget\\Url::ajax_contact', [[]], null, null, + (parameters) => + { + for(const email in contact_requests) + { + parameters[0].push(email); + } + }).then((result) => + { + for(const email in contact_requests) + { + email_cache[email] = result[email]; + contact_requests[email].forEach((resolve) => + { + resolve(result[email]); + }); + } + contact_request = null; + contact_requests = {}; + }); + } + if(typeof contact_requests[email] === 'undefined') + { + contact_requests[email] = []; + } + return new Promise(resolve => + { + contact_requests[email].push(resolve); + }); +} + +/** + * if we have a "name " value split it into name & email + * @param email_string + * + * @return {name:string, email:string} + */ +export function splitEmail(email_string) : { name : string, email : string } +{ + let split = {name: "", email: email_string}; + if(email_string && email_string.indexOf('<') !== -1) + { + const parts = email_string.split('<'); + if(parts[0]) + { + split.email = parts[1].substring(0, parts[1].length - 1).trim(); + split.name = parts[0].trim(); + // remove quotes + while((split.name[0] === '"' || split.name[0] === "'") && split.name[0] === split.name.substr(-1)) + { + split.name = split.name.substring(1, split.name.length - 1); + } + } + else // --> email + { + split.email = parts[1].substring(0, email_string.length - 1); + } + } + return split; +} + +/** + * Format an email address according to user preference + * + * @param address + * @param {"full" | "email" | "name" | "domain"} emailDisplayFormat + * @returns {any} + */ +export async function formatEmailAddress(address : string, emailDisplayFormat? : "full" | "email" | "name" | "domain") : Promise +{ + if(!address || !address.trim()) + { + return ""; + } + + if(!emailDisplayFormat) + { + emailDisplayFormat = _getEmailDisplayPreference(); + } + + const split = splitEmail(address); + let content = address; + let contact; + if(!split.name && (contact = await checkContact(address))) + { + split.name = contact.n_fn; + } + + if(split.name) + { + switch(emailDisplayFormat) + { + case "full": + content = split.name + " <" + split.email + ">"; + break; + case "email": + content = split.email; + break; + case "name": + default: + content = split.name; + break; + case "domain": + content = split.name + " (" + split.email.split("@").pop() + ")"; + break; + } + } + return content; +} \ No newline at end of file diff --git a/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts b/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts index d44b35c6e4..cbbb764af4 100644 --- a/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts +++ b/api/js/etemplate/Et2Select/Tag/Et2EmailTag.ts @@ -6,11 +6,13 @@ * @link https://www.egroupware.org * @author Nathan Gray */ -import {css, html, nothing, PropertyValues, TemplateResult} from "lit"; +import {css, html, nothing, TemplateResult} from "lit"; import {property} from "lit/decorators/property.js"; import {classMap} from "lit/directives/class-map.js"; import shoelace from "../../Styles/shoelace"; import {Et2Tag} from "./Et2Tag"; +import {checkContact, ContactInfo, formatEmailAddress} from "../../Et2Email/utils"; +import {until} from "lit/directives/until.js"; /** * Display a single email address @@ -23,8 +25,6 @@ import {Et2Tag} from "./Et2Tag"; export class Et2EmailTag extends Et2Tag { - private static email_cache : { [address : string] : ContactInfo | false } = {}; - static get styles() { return [ @@ -113,45 +113,6 @@ export class Et2EmailTag extends Et2Tag this.removeEventListener("mouseleave", this.handleMouseLeave); } - static contact_request : Promise; - static contact_requests : { [key: string]: Array; } = {}; - - public checkContact(email : string) : Promise - { - if(typeof Et2EmailTag.email_cache[email] !== "undefined") - { - return Promise.resolve(Et2EmailTag.email_cache[email]); - } - if (!Et2EmailTag.contact_request) - { - Et2EmailTag.contact_request = this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Url::ajax_contact', [[]], null, null, - (parameters) => { - for(const email in Et2EmailTag.contact_requests) - { - parameters[0].push(email); - } - }).then((result) => - { - for(const email in Et2EmailTag.contact_requests) - { - Et2EmailTag.email_cache[email] = result[email]; - Et2EmailTag.contact_requests[email].forEach((resolve) => { - resolve(result[email]); - }); - } - Et2EmailTag.contact_request = null; - Et2EmailTag.contact_requests = {}; - }); - } - if (typeof Et2EmailTag.contact_requests[email] === 'undefined') - { - Et2EmailTag.contact_requests[email] = []; - } - return new Promise(resolve => { - Et2EmailTag.contact_requests[email].push(resolve); - }); - } - handleMouseEnter(e : MouseEvent) { this.shadowRoot.querySelector(".tag").classList.add("contact_plus"); @@ -176,7 +137,7 @@ export class Et2EmailTag extends Et2Tag handleContactMouseDown(e : MouseEvent) { e.stopPropagation(); - this.checkContact(this.value).then((result) => + checkContact(this.value).then((result) => { this.egw().open((result).id, 'addressbook', 'view', { title: (result).n_fn, @@ -194,49 +155,13 @@ export class Et2EmailTag extends Et2Tag { return this.shadowRoot.querySelector(".tag__prefix"); } - - protected update(changedProperties : PropertyValues) - { - super.update(changedProperties); - - if(changedProperties.has("value") && this.value) - { - // Send the request - this.checkContact(this.value).then((result) => - { - this.requestUpdate(); - }); - } - } - public _contentTemplate() : TemplateResult { - const split = Et2EmailTag.splitEmail(this.value); - - let content = this.value; - if(split.name) - { - switch(this.emailDisplay) - { - case "full": - content = split.name + " <" + split.email + ">"; - break; - case "email": - content = split.email; - break; - case "name": - default: - content = split.name; - break; - case "domain": - content = split.name + " (" + split.email.split("@").pop() + ")"; - break; - } - } + const content = formatEmailAddress(this.value, this.emailDisplay); return html` - ${content} + ${until(content, this.value)} `; } @@ -245,17 +170,18 @@ export class Et2EmailTag extends Et2Tag let classes = { "tag__prefix": true, } - let button_or_avatar; - // Show the lavatar for the contact - if(this.value && Et2EmailTag.email_cache[this.value]) + let button_or_avatar = checkContact(this.value).then((option) => { - classes['tag__has_contact'] = true; - // lavatar uses a size property, not a CSS variable - let style = getComputedStyle(this); - const option = Et2EmailTag.email_cache[this.value]; + let button_or_avatar; + if(typeof option == "object") + { + // Show the lavatar for the contact + classes['tag__has_contact'] = true; - button_or_avatar = html` + // lavatar uses a size property, not a CSS variable + let style = getComputedStyle(this); + button_or_avatar = html` `; - } - else - { - // Show a button to add as new contact - classes['tag__has_plus'] = true; - button_or_avatar = html` - `; - } + } - return html` - + return html` ${button_or_avatar} - `; - } + `; + }); - /** - * if we have a "name " value split it into name & email - * @param email_string - * - * @return {name:string, email:string} - */ - public static splitEmail(email_string) : { name : string, email : string } - { - let split = {name: "", email: email_string}; - if(email_string && email_string.indexOf('<') !== -1) - { - const parts = email_string.split('<'); - if(parts[0]) - { - split.email = parts[1].substring(0, parts[1].length - 1).trim(); - split.name = parts[0].trim(); - // remove quotes - while((split.name[0] === '"' || split.name[0] === "'") && split.name[0] === split.name.substr(-1)) - { - split.name = split.name.substring(1, split.name.length - 1); - } - } - else // --> email - { - split.email = parts[1].substring(0, email_string.length - 1); - } - } - return split; + return html` + ${until(button_or_avatar, html` + + + + `)}`; } } -interface ContactInfo -{ - id : number, - n_fn : string, - photo? : string -} customElements.define("et2-email-tag", Et2EmailTag); \ No newline at end of file diff --git a/api/js/etemplate/Et2Url/Et2UrlEmailReadonly.ts b/api/js/etemplate/Et2Url/Et2UrlEmailReadonly.ts index f17032eb0d..1738ad6f6e 100644 --- a/api/js/etemplate/Et2Url/Et2UrlEmailReadonly.ts +++ b/api/js/etemplate/Et2Url/Et2UrlEmailReadonly.ts @@ -11,68 +11,33 @@ import {IsEmail} from "../Validators/IsEmail"; import {Et2UrlEmail} from "./Et2UrlEmail"; import {Et2UrlReadonly} from "./Et2UrlReadonly"; +import {property} from "lit/decorators/property.js"; +import {formatEmailAddress, splitEmail} from "../Et2Email/utils"; /** * @customElement et2-url-email_ro */ export class Et2UrlEmailReadonly extends Et2UrlReadonly { - /** @type {any} */ - static get properties() - { - return { - ...super.properties, - /** - * Show full email address if true otherwise show only name and put full address as statustext/tooltip - */ - fullEmail: { - type: Boolean, - }, - /** - * Show icon to add email as contact to addressbook - * @ToDo - */ - contactPlus: { - type: Boolean, - }, - /** - * set to true to always show just the email - * Mutually exclusive with fullEmail! - */ - onlyEmail: { - type: Boolean - } - }; - } + /** + * What to display for the selected email addresses + * + * - full: "Mr Test User + * - name: "Mr Test User" + * - domain: "Mr Test User (example.com)" + * - email: "test@example.com" + * + * If name is unknown, we'll use the email instead. + */ + @property({type: String}) + emailDisplay : "full" | "email" | "name" | "domain"; set value(val : string) { this._value = val; - // check if we have a "name " value and only show name - if (!this.fullEmail && val && val.indexOf('<') !== -1) - { - const parts = val.split('<'); - if (parts[0] && !this.onlyEmail) - { - super.statustext = parts[1].substring(0, parts[1].length-1); - val = parts[0].trim(); - // remove quotes - if ((val[0] === '"' || val[0] === "'" ) && val[0] === val.substr(-1)) - { - val = val.substring(1, val.length-1); - } - } - else // --> email - { - super.statustext = ''; - val = parts[1].substring(0, val.length-1); - } - } - else - { - super.statustext = ''; - } - super.value = val; + const split = splitEmail(this._value); + super.statustext = split.name ? split.email : ""; + formatEmailAddress(val, this.emailDisplay).then((value) => super.value = value); } get value()