snappymail/dev/Component/EmailAddresses.js

438 lines
10 KiB
JavaScript

import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email';
import { addressparser } from 'Mime/Address';
const contentType = 'snappymail/emailaddress',
getAddressKey = li => li?.emailaddress?.key,
parseEmailLine = line => addressparser(line).map(item =>
(item.name || item.email)
? new EmailModel(item.email, item.name) : null
).filter(v => v),
splitEmailLine = line => {
const result = [];
let exists = false;
addressparser(line).forEach(item => {
const address = (item.name || item.email)
? new EmailModel(item.email, item.name)
: null;
if (address?.email) {
exists = true;
}
result.push(address ? address.toLine() : item.name);
});
return exists ? result : null;
};
let dragAddress, datalist;
// mailbox-list
export class EmailAddressesComponent {
constructor(element, options) {
if (!datalist) {
datalist = createElement('datalist',{id:"emailaddresses-datalist"});
doc.body.append(datalist);
}
const self = this,
input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off"}),
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress?.li.parentNode !== self.ul,
fnDrag = e => validDropzone() && e.preventDefault();
self.element = element;
self.options = Object.assign({
focusCallback : null,
// simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality
autoCompleteSource : '',
onChange : null
}, options);
self._chosenValues = [];
self._lastEdit = '';
// Create the elements
self.ul = createElement('ul',{class:"emailaddresses"});
addEventsListeners(self.ul, {
click: e => self._focus(e),
dblclick: e => self._editTag(e),
dragenter: fnDrag,
dragover: fnDrag,
drop: e => {
if (validDropzone() && dragAddress.value) {
e.preventDefault();
dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
}
}
});
self.input = input;
addEventsListeners(input, {
focus: () => {
self._focusTrigger(true);
input.value || self._resetDatalist();
},
blur: () => {
// prevent autoComplete menu click from causing a false 'blur'
self._parseInput(true);
self._focusTrigger(false);
},
keydown: e => {
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
// if our input contains no value and backspace has been pressed, select the last tag
var lastTag = self.inputCont.previousElementSibling;
if (lastTag && (!input.value
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
) {
e.preventDefault();
lastTag.querySelector('a').focus();
}
self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
self._parseInput(true);
}
},
input: () => {
self._parseInput();
self._updateDatalist();
}
});
// define starting placeholder
if (element.placeholder) {
input.placeholder = element.placeholder;
}
self.inputCont = createElement('li',{class:"emailaddresses-input"});
self.inputCont.append(input);
self.ul.append(self.inputCont);
element.replaceWith(self.ul);
// if instantiated input already contains a value, parse that junk
if (element.value.trim()) {
self._parseValue(element.value);
}
self._updateDatalist = self.options.autoCompleteSource
? (() => {
let value = input.value.trim();
if (datalist.inputValue !== value) {
datalist.inputValue = value;
value.length && self.options.autoCompleteSource(
value,
items => {
self._resetDatalist();
let chars = value.length;
items?.forEach(item => {
datalist.append(new Option(item));
chars = Math.max(chars, item.length);
});
// https://github.com/the-djmaze/snappymail/issues/368 and #513
chars *= 8;
if (input.clientWidth < chars) {
input.style.width = chars + 'px';
}
}
)
}
}).throttle(500)
: () => 0;
}
_focusTrigger(bValue) {
this.ul.classList.toggle('emailaddresses-focused', bValue);
this.options.focusCallback(bValue);
}
_resetDatalist() {
datalist.textContent = '';
}
_parseInput(force) {
let val = this.input.value;
if ((force || val.includes(',') || val.includes(';')
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val)))
&& this._parseValue(val)) {
this.input.value = '';
}
this._resizeInput();
}
_parseValue(val) {
if (val) {
const self = this,
v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
values = (hook || [val]).map(value => parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
if (values.length) {
values.forEach(a => {
var v = a[0].trim(),
exists = false,
lastIndex = -1,
obj = {
key : '',
obj : null,
value : ''
};
self._chosenValues.forEach((vv, kk) => {
if (vv.value === self._lastEdit) {
lastIndex = kk;
}
exists |= vv.value === v;
});
if (v !== '' && a[1] && !exists) {
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
obj.value = v;
obj.obj = a[1];
if (-1 < lastIndex) {
self._chosenValues.splice(lastIndex, 0, obj);
} else {
self._chosenValues.push(obj);
}
self._lastEdit = '';
self._renderTags();
}
});
if (1 === values.length && '' === values[0] && '' !== self._lastEdit) {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
return true;
}
}
}
// the input dynamically resizes based on the length of its value
_resizeInput() {
let input = this.input;
if (input.clientWidth < input.scrollWidth) {
input.style.width = (input.scrollWidth + 20) + 'px';
}
}
_editTag(ev) {
var li = ev.target.closest('li'),
tagKey = getAddressKey(li);
if (!tagKey) {
return true;
}
var self = this,
tagName = '',
oPrev = null,
next = false
;
self._chosenValues.forEach(v => {
if (v.key === tagKey) {
tagName = v.value;
next = true;
} else if (next && !oPrev) {
oPrev = v;
}
});
if (oPrev)
{
self._lastEdit = oPrev.value;
}
li.after(self.inputCont);
self.input.value = tagName;
setTimeout(() => self.input.select(), 100);
self._removeTag(ev, li);
self._resizeInput(ev);
}
_buildValue() {
return this._chosenValues.map(v => v.value).join(',');
}
_setValue(value) {
if (this.element.value !== value) {
this.element.value = value;
this.options.onChange(value);
}
}
_simpleEmailMatch(value) {
// A very SIMPLE test to check if the value might be an email
const val = value.trim();
return /^[^@]*<[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}>$/g.test(val)
|| /^[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}$/g.test(val);
}
_renderTags() {
let self = this;
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
self._chosenValues.forEach(v => {
if (v.obj) {
let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}),
el = createElement('span');
el.append(v.obj.toLine(true));
li.append(el);
el = createElement('a',{href:'#', class:'ficon'});
el.append('✖');
addEventsListeners(el, {
click: e => self._removeTag(e, li),
focus: () => li.className = 'emailaddresses-selected',
blur: () => li.className = null,
keydown: e => {
switch (e.key) {
case 'Delete':
case 'Backspace':
self._removeTag(e, li);
break;
// 'e' - edit tag (removes tag and places value into visible input
case 'e':
case 'Enter':
self._editTag(e);
break;
case 'ArrowLeft':
// select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) {
previous.querySelector('a').focus();
} else {
self.focus();
}
break;
case 'ArrowRight':
// select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) {
next.querySelector('a').focus();
} else {
this.focus();
}
break;
case 'ArrowDown':
self._focus(e);
break;
}
}
});
li.append(el);
li.emailaddress = v;
addEventsListeners(li, {
dragstart: e => {
dragAddress = {
source: self,
li: li,
value: li.emailaddress.obj.toLine()
};
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
e.dataTransfer.setData('text/plain', contentType);
// e.dataTransfer.setDragImage(li, 0, 0);
e.dataTransfer.effectAllowed = 'move';
li.style.opacity = 0.25;
},
dragend: () => {
dragAddress = null;
li.style.cssText = '';
}
});
self.inputCont.before(li);
}
});
}
_removeTag(ev, li) {
ev.preventDefault();
var key = getAddressKey(li),
self = this,
indexFound = self._chosenValues.findIndex(v => key === v.key);
indexFound > -1 && self._chosenValues.splice(indexFound, 1);
self._setValue(self._buildValue());
li.remove();
setTimeout(() => self.input.focus(), 100);
}
_removeDraggedTag(li) {
var
key = getAddressKey(li),
self = this,
indexFound = self._chosenValues.findIndex(v => key === v.key)
;
if (-1 < indexFound) {
self._chosenValues.splice(indexFound, 1);
self._setValue(self._buildValue());
}
li.remove();
}
focus () {
this.input.focus();
}
blur() {
this.input.blur();
}
_focus(ev) {
var li = ev.target.closest('li');
if (getAddressKey(li)) {
li.querySelector('a').focus();
} else {
this.focus();
}
}
set value(value) {
var self = this;
if (self.element.value !== value) {
// self.input.value = '';
// self._resizeInput();
self._chosenValues = [];
self._renderTags();
self._parseValue(self.element.value = value);
}
}
}