mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-11-14 11:44:54 +08:00
437 lines
10 KiB
JavaScript
437 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);
|
|
}
|
|
}
|
|
}
|