/* 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