/** * Copyright since 2007 PrestaShop SA and Contributors * PrestaShop is an International Registered Trademark & Property of PrestaShop SA * * NOTICE OF LICENSE * * This source file is subject to the Open Software License (OSL 3.0) * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: * https://opensource.org/licenses/OSL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * * DISCLAIMER * * Do not edit or add to this file if you wish to upgrade PrestaShop to newer * versions in the future. If you wish to customize PrestaShop for your * needs please refer to https://devdocs.prestashop.com/ for more information. * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ import ComponentsMap from '@components/components-map'; import {EventEmitter} from './event-emitter'; const {$} = window; /** * This class init TinyMCE instances in the back-office. It is wildly inspired by * the scripts from js/admin And it actually loads TinyMCE from the js/tiny_mce * folder along with its modules. One improvement could be to install TinyMCE via * npm and fully integrate in the back-office theme. */ class TinyMCEEditor { constructor(options) { const opts = options || {}; this.tinyMCELoaded = false; if (typeof opts.baseAdminUrl === 'undefined') { if (typeof window.baseAdminDir !== 'undefined') { opts.baseAdminUrl = window.baseAdminDir; } else { const pathParts = window.location.pathname.split('/'); pathParts.every((pathPart) => { if (pathPart !== '') { opts.baseAdminUrl = `/${pathPart}/`; return false; } return true; }); } } if (typeof opts.langIsRtl === 'undefined') { opts.langIsRtl = typeof window.lang_is_rtl !== 'undefined' ? window.lang_is_rtl === '1' : false; } this.setupTinyMCE(opts); } /** * Initial setup which checks if the tinyMCE library is already loaded. * * @param config */ setupTinyMCE(config) { if (typeof tinyMCE === 'undefined') { this.loadAndInitTinyMCE(config); } else { this.initTinyMCE(config); } } /** * Prepare the config and init all TinyMCE editors * * @param config */ initTinyMCE(config) { const cfg = { selector: '.rte', plugins: 'align colorpicker link image filemanager table media placeholder lists advlist code table autoresize', browser_spellcheck: true, toolbar1: /* eslint-disable-next-line max-len */ 'code,colorpicker,bold,italic,underline,strikethrough,blockquote,link,align,bullist,numlist,table,image,media,formatselect', toolbar2: '', language: window.iso_user, external_filemanager_path: `${config.baseAdminUrl}filemanager/`, filemanager_title: 'File manager', external_plugins: { filemanager: `${config.baseAdminUrl}filemanager/plugin.min.js`, }, content_style: config.langIsRtl ? 'body {direction:rtl;}' : '', skin: 'prestashop', mobile: { theme: 'mobile', plugins: ['lists', 'align', 'link', 'table', 'placeholder', 'advlist', 'code'], toolbar: /* eslint-disable-next-line max-len */ 'undo code colorpicker bold italic underline strikethrough blockquote link align bullist numlist table formatselect styleselect', }, menubar: false, statusbar: false, relative_urls: false, convert_urls: false, entity_encoding: 'raw', extended_valid_elements: 'em[class|name|id],@[role|data-*|aria-*]', valid_children: '+*[*]', valid_elements: '*[*]', rel_list: [{title: 'nofollow', value: 'nofollow'}], editor_selector: ComponentsMap.tineMceEditor.selectorClass, init_instance_callback: () => { this.changeToMaterial(); }, setup: (editor) => { this.setupEditor(editor); }, ...config, }; if (typeof window.defaultTinyMceConfig !== 'undefined') { Object.assign(cfg, window.defaultTinyMceConfig); } if (typeof cfg.editor_selector !== 'undefined') { cfg.selector = `.${cfg.editor_selector}`; } // Change icons in popups $('body').on('click', '.mce-btn, .mce-open, .mce-menu-item', () => { this.changeToMaterial(); }); window.tinyMCE.init(cfg); this.watchTabChanges(cfg); } /** * Setup TinyMCE editor once it has been initialized * * @param editor */ setupEditor(editor) { editor.on('loadContent', (event) => { this.handleCounterTiny(event.target.id); }); editor.on('change', (event) => { window.tinyMCE.triggerSave(); this.handleCounterTiny(event.target.id); }); editor.on('blur', () => { window.tinyMCE.triggerSave(); }); EventEmitter.emit('tinymceEditorSetup', { editor, }); } /** * When the editor is inside a tab it can cause a bug on tab switching. * So we check if the editor is contained in a navigation and refresh the editor when its * parent tab is shown. * * @param config */ watchTabChanges(config) { $(config.selector).each((index, textarea) => { const translatedField = $(textarea).closest('.translation-field'); const tabContainer = $(textarea).closest('.translations.tabbable'); if (translatedField.length && tabContainer.length) { const textareaLocale = translatedField.data('locale'); const textareaLinkSelector = `.nav-item a[data-locale="${textareaLocale}"]`; $(textareaLinkSelector, tabContainer).on('shown.bs.tab', () => { const form = $(textarea).closest('form'); const editor = window.tinyMCE.get(textarea.id); if (editor) { // Reset content to force refresh of editor editor.setContent(editor.getContent()); } EventEmitter.emit('languageSelected', { selectedLocale: textareaLocale, form, }); }); } }); EventEmitter.on('languageSelected', (data) => { const textareaLinkSelector = `.nav-item a[data-locale="${data.selectedLocale}"]`; $(textareaLinkSelector).click(); }); } /** * Loads the TinyMCE javascript library and then init the editors * * @param config */ loadAndInitTinyMCE(config) { if (this.tinyMCELoaded) { return; } this.tinyMCELoaded = true; const pathArray = config.baseAdminUrl.split('/'); pathArray.splice(pathArray.length - 2, 2); const finalPath = pathArray.join('/'); window.tinyMCEPreInit = {}; window.tinyMCEPreInit.base = `${finalPath}/js/tiny_mce`; window.tinyMCEPreInit.suffix = '.min'; $.getScript(`${finalPath}/js/tiny_mce/tinymce.min.js`, () => { this.setupTinyMCE(config); }); } /** * Replace initial TinyMCE icons with material icons */ changeToMaterial() { const materialIconAssoc = { 'mce-i-code': 'code', 'mce-i-none': 'format_color_text', 'mce-i-bold': 'format_bold', 'mce-i-italic': 'format_italic', 'mce-i-underline': 'format_underlined', 'mce-i-strikethrough': 'format_strikethrough', 'mce-i-blockquote': 'format_quote', 'mce-i-link': 'link', 'mce-i-alignleft': 'format_align_left', 'mce-i-aligncenter': 'format_align_center', 'mce-i-alignright': 'format_align_right', 'mce-i-alignjustify': 'format_align_justify', 'mce-i-bullist': 'format_list_bulleted', 'mce-i-numlist': 'format_list_numbered', 'mce-i-image': 'image', 'mce-i-table': 'grid_on', 'mce-i-media': 'video_library', 'mce-i-browse': 'attachment', 'mce-i-checkbox': '', }; $.each(materialIconAssoc, (index, value) => { $(`.${index}`).replaceWith(value); }); } /** * Updates the characters counter. This counter is used for front but if you don't want to encounter Validation * problems you should be in sync with the TinyMceMaxLengthValidator PHP class. Both codes must behave the same * way. * * @param id */ handleCounterTiny(id) { const textarea = $(`#${id}`); const counter = textarea.attr('counter'); const counterType = textarea.attr('counter_type'); const editor = window.tinyMCE.get(id); const max = editor.getBody() ? editor.getBody().textContent.length : 0; textarea .parent() .find('span.currentLength') .text(max); if (counterType !== 'recommended' && max > counter) { textarea .parent() .find('span.maxLength') .addClass('text-danger'); } else { textarea .parent() .find('span.maxLength') .removeClass('text-danger'); } } } export default TinyMCEEditor;