/* eslint max-len: 0 */
(win => {
const rl = win.rl;
if (!rl) {
return;
}
rl.registerWYSIWYG('CompactComposer', (owner, container, onReady) => {
const editor = new CompactComposer(container);
onReady(editor);
});
const doc = win.document;
addEventListener('rl-view-model', e => {
const vm = e.detail;
if ('PopupsCompose' === vm.viewModelTemplateID && rl.settings.get('editorWysiwyg') === 'CompactComposer') {
// add visible binding to the label
const bodyLabel = vm.querySelector('.tabs label[for="tab-body"]');
bodyLabel.dataset.bind = 'visible: canMailvelope';
// re-apply the binding
const labelNext = bodyLabel.nextElementSibling;
ko.removeNode(bodyLabel);
labelNext.parentElement.insertBefore(bodyLabel, labelNext);
ko.applyBindingAccessorsToNode(bodyLabel, null, vm);
// Now move the attachments tab to the bottom of the screen
const area = vm.querySelector('.tabs .attachmentAreaParent');
vm.querySelector('.tabs input[value="attachments"]').remove();
vm.querySelector('.tabs label[for="tab-attachments"]').remove();
area.querySelector('.no-attachments-desc').remove();
area.classList.add('compact');
vm.viewModelDom.append(area);
// Add and re-apply the bindings for the attachment-place
const place = area.querySelector('.b-attachment-place');
ko.removeNode(place);
area.insertBefore(place, area.firstElementChild);
place.dataset.bind = 'visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}';
ko.applyBindingAccessorsToNode(place, null, vm);
// 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
// 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) => {
return rl.Utils.cleanHtml(event.detail.html).html;
},
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;
container.classList.add('CompactComposer');
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);
});
wysiwyg.addEventListener('focus', () => {
const range = this.squire.getSelection();
if (range.collapsed && range.startContainer === wysiwyg) {
// when the caret is directly in the wysiwyg a bunch of stuff
// (like lists, blockquotes, etc...) do not work,
// so we need to place it inside the nearest element
if (wysiwyg.children[range.startOffset] !== undefined) {
const newRange = document.createRange();
newRange.setStart(wysiwyg.children[range.startOffset], 0);
this.squire.setSelection(newRange);
}
}
});
// 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