Refactor email address formatting and use it in Et2EmailTag and Et2UrlEmailReadonly

This commit is contained in:
nathan 2024-01-16 15:29:12 -07:00
parent 0b20751602
commit 84fb37214a
3 changed files with 215 additions and 187 deletions

View File

@ -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<void | boolean>;
let contact_requests : { [key : string] : Array<Function>; } = {};
/**
* 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<boolean | ContactInfo>}
*/
export function checkContact(email : string) : Promise<false | ContactInfo>
{
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 <email>" 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> --> 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<string>
{
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;
}

View File

@ -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<any>;
static contact_requests : { [key: string]: Array<Function>; } = {};
public checkContact(email : string) : Promise<boolean | ContactInfo>
{
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((<ContactInfo>result).id, 'addressbook', 'view', {
title: (<ContactInfo>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`
<span part="content" class="tag__content" title="${this.value}">
${content}
${until(content, this.value)}
</span>`;
}
@ -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`
<et2-lavatar slot="prefix" exportparts="image" part="icon" tabindex="-1"
@mousedown=${this.handleContactMouseDown}
.size=${style.getPropertyValue("--icon-width")}
@ -265,61 +191,32 @@ export class Et2EmailTag extends Et2Tag
statustext="${this.egw().lang("Open existing contact") + ": " + option.n_fn}"
>
</et2-lavatar>`;
}
else
{
// Show a button to add as new contact
classes['tag__has_plus'] = true;
button_or_avatar = html`
<et2-button-icon image="add" tabindex="-1" @click=${this.handleMouseClick}
}
else
{
// Show a button to add as new contact
classes['tag__has_plus'] = true;
button_or_avatar = html`
<et2-button-icon image="add" tabindex="-1" @click=${this.handleMouseClick} .noSubmit=${true}
label="${this.egw().lang("Add a new contact")}"
statustext="${this.egw().lang("Add a new contact")}">
</et2-button-icon>`;
}
}
return html`
<span part="prefix" class=${classMap(classes)}>
return html`<span part="prefix" class=${classMap(classes)}>
<slot name="prefix">
</slot>
${button_or_avatar}
</span>`;
}
</span>`;
});
/**
* if we have a "name <email>" 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> --> email
{
split.email = parts[1].substring(0, email_string.length - 1);
}
}
return split;
return html`
${until(button_or_avatar, html`
<span part="prefix" class=${classMap(classes)}>
<slot name="prefix"></slot>
<sl-spinner></sl-spinner>
</span>`)}`;
}
}
interface ContactInfo
{
id : number,
n_fn : string,
photo? : string
}
customElements.define("et2-email-tag", Et2EmailTag);

View File

@ -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 <test@example.com>
* - 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 <email>" 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> --> 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()