/* eslint max-len: 0 */
(win => {
addEventListener('rl-view-model.create', e => {
if (e.detail.viewModelTemplateID === 'PopupsCompose') {
// There is a better way to do this probably,
// but we need this for drag and drop to work
e.detail.attachmentsArea = e.detail.bodyArea;
}
});
const doc = win.document;
const rl = win.rl;
const
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
// TODO: labels translations
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ',
createElement = name => doc.createElement(name),
tpl = createElement('template'),
trimLines = html => html.trim().replace(/^(
\s* \s*<\/div>)+/, '').trim(),
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
frag.append(...parent.childNodes);
return frag;
},
/**
* @param {Array} data
* @param {String} prop
*/
getByProp = (data, prop) => {
for (let i = 0; i < data.length; i++) {
const outer = data[i];
if (outer.hasOwnProperty(prop)) {
return outer;
}
if (outer.items && Array.isArray(outer.items)) {
const item = outer.items.find(item => item.prop === prop);
if (item) {
return item;
}
}
}
throw new Error('item with prop ' + prop + ' not found');
},
SquireDefaultConfig = {
/*
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html) => {
tpl.innerHTML = (html || '')
.replace(/<\/?(BODY|HTML)[^>]*>/gi, '')
.replace(//g, '')
.replace(/]*>\s*<\/span>/gi, '')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
return tpl.content;
}
},
pasteSanitizer = (event) => {
const frag = event.detail.fragment;
frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
frag.querySelectorAll(removeElements).forEach(el => el.remove());
frag.querySelectorAll('*').forEach(el => {
if (!el.matches(allowedElements)) {
el.replaceWith(getFragmentOfChildren(el));
} else if (el.hasAttributes()) {
[...el.attributes].forEach(attr => {
let name = attr.name.toLowerCase();
if (!allowedAttributes.includes(name)) {
el.removeAttribute(name);
}
});
}
});
},
pasteImageHandler = (e, squire) => {
const items = [...e.detail.clipboardData.items];
const imageItems = items.filter((item) => /image/.test(item.type));
if (!imageItems.length) {
return false;
}
let reader = new FileReader();
reader.onload = (loadEvent) => {
squire.insertImage(loadEvent.target.result);
};
reader.readAsDataURL(imageItems[0].getAsFile());
};
class CompactComposer {
constructor(container) {
const
plain = createElement('textarea'),
wysiwyg = createElement('div'),
toolbar = createElement('div'),
squire = new win.Squire2(wysiwyg, SquireDefaultConfig);
this.container = container;
plain.className = 'squire-plain';
wysiwyg.className = 'squire-wysiwyg';
wysiwyg.dir = 'auto';
this.mode = ''; // 'plain' | 'wysiwyg'
this.squire = squire;
this.plain = plain;
this.wysiwyg = wysiwyg;
this.toolbar = toolbar;
toolbar.className = 'squire-toolbar btn-toolbar';
const actions = this.#makeActions(squire, toolbar);
this.squire.addEventListener('willPaste', pasteSanitizer);
this.squire.addEventListener('pasteImage', (e) => {
pasteImageHandler(e, squire);
});
// squire.addEventListener('focus', () => shortcuts.off());
// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
const fontFamilySelect = getByProp(actions, 'fontFamily').element;
const fontSizeAction = getByProp(actions, 'fontSize');
/**
* @param {string} fontName
* @return {string}
*/
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
/** @type {string[]} - lower cased array of available font families*/
const fontFamiliesLowerCase = Object.values(fontFamilySelect.options).map(option => option.value.toLowerCase());
/**
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
* font-family: 'Times New Roman', Times, serif; }
* so let's find the best match squire.getRoot()'s font
* it will also help to properly handle generic font names like 'sans-serif'
* @type {number}
*/
let defaultFontFamilyIndex = 0;
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
fontFamiliesLowerCase.some((family, index) => {
const matchFound = family.split(',').some(availableFontName => {
const normalizedFontName = normalizeFontName(availableFontName);
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
});
if (matchFound) {
defaultFontFamilyIndex = index;
}
return matchFound;
});
/**
* Instead of comparing whole 'font-family' strings,
* we are going to look for individual font names, because we might be
* editing a Draft started in another email client for example
*
* @type {Object.}
*/
const fontNamesMap = {};
/**
* @param {string} fontFamily
* @param {number} index
*/
const processFontFamilyString = (fontFamily, index) => {
fontFamily.split(',').forEach(fontName => {
const key = normalizeFontName(fontName);
if (fontNamesMap[key] === undefined) {
fontNamesMap[key] = index;
}
});
};
// first deal with the default font family
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
// and now with the rest of the font families
fontFamiliesLowerCase.forEach((fontFamily, index) => {
if (index !== defaultFontFamilyIndex) {
processFontFamilyString(fontFamily, index);
}
});
// -----
let ignoreNextSelectEvent = false;
squire.addEventListener('pathChange', e => {
const tokensMap = this.buildTokensMap(e.detail);
if (tokensMap.has('__selection__')) {
ignoreNextSelectEvent = false;
return;
}
this.indicators.forEach((indicator) => {
indicator.element.classList.toggle('active', indicator.selectors.some(selector => tokensMap.has(selector)));
});
let familySelectedIndex = defaultFontFamilyIndex;
const fontFamily = tokensMap.get('__font_family__');
if (fontFamily) {
familySelectedIndex = -1; // show empty select if we don't know the font
const fontNames = fontFamily.split(',');
for (let i = 0; i < fontNames.length; i++) {
const index = fontNamesMap[normalizeFontName(fontNames[i])];
if (index !== undefined) {
familySelectedIndex = index;
break;
}
}
}
fontFamilySelect.selectedIndex = familySelectedIndex;
let sizeSelectedIndex = fontSizeAction.defaultValueIndex;
const fontSize = tokensMap.get('__font_size__');
if (fontSize) {
// -1 is ok because it will just show a blank