import grapesjs from 'grapesjs'; import grapesjsmjml from 'grapesjs-mjml'; import grapesjsnewsletter from 'grapesjs-preset-newsletter'; import grapesjswebpage from 'grapesjs-preset-webpage'; import grapesjsblocksbasic from 'grapesjs-blocks-basic'; import grapesjscomponentcountdown from 'grapesjs-component-countdown'; import grapesjsnavbar from 'grapesjs-navbar'; import grapesjscustomcode from 'grapesjs-custom-code'; import grapesjstouch from 'grapesjs-touch'; import grapesjstuiimageeditor from 'grapesjs-tui-image-editor'; import grapesjsstylebg from 'grapesjs-style-bg'; import grapesjspostcss from 'grapesjs-parser-postcss'; import contentService from 'grapesjs-preset-mautic/dist/content.service'; import grapesjsmautic from 'grapesjs-preset-mautic'; import editorFontsService from 'grapesjs-preset-mautic/dist/editorFonts/editorFonts.service'; import 'grapesjs-plugin-ckeditor5'; import StorageService from "./storage.service"; // for local dev // import contentService from '../../../../../../grapesjs-preset-mautic/src/content.service'; // import grapesjsmautic from '../../../../../../grapesjs-preset-mautic/src'; import CodeModeButton from './codeMode/codeMode.button'; import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service'; export default class BuilderService { editor; assets; uploadPath; deletePath; storageService; /** * @param {*} assets */ constructor(assets) { if (!assets.conf.uploadPath) { throw Error('No uploadPath found'); } if (!assets.conf.deletePath) { throw Error('No deletePath found'); } this.assets = assets.files; this.uploadPath = assets.conf.uploadPath; this.deletePath = assets.conf.deletePath; } /** * Initialize GrapesJsBuilder * * @param object */ setListeners() { if (!this.editor) { throw Error('No editor found'); } // Why would we not want to keep the history? // // this.editor.on('load', () => { // const um = this.editor.UndoManager; // // Clear stack of undo/redo // um.clear(); // }); const keymaps = this.editor.Keymaps; let allKeymaps; if (mauticEditorFonts) { this.editor.on('load', () => editorFontsService.loadEditorFonts(this.editor)); } this.editor.on('modal:open', () => { // Save all keyboard shortcuts allKeymaps = { ...keymaps.getAll() }; // Remove keyboard shortcuts to prevent launch behind popup keymaps.removeAll(); }); this.editor.on('modal:close', () => { // ReMap keyboard shortcuts on modal close Object.keys(allKeymaps).map((objectKey) => { const shortcut = allKeymaps[objectKey]; keymaps.add(shortcut.id, shortcut.keys, shortcut.handler); return keymaps; }); }); this.editor.on('asset:remove', (response) => { // Delete file on server mQuery.ajax({ url: this.deletePath, data: { filename: response.getFilename() }, }); }); const triggerBuilderHide = () => { // trigger hide event on DOM element mQuery('.builder').trigger('builder:hide', [this.editor]); // trigger hide event on editor instance this.editor.trigger('hide'); }; this.editor.on('run:mautic-editor-page-html-close', triggerBuilderHide); this.editor.on('run:mautic-editor-email-html-close', triggerBuilderHide); this.editor.on('run:mautic-editor-email-mjml-close', triggerBuilderHide); // add offset to flashes container for better UI visibility when builder is on this.editor.on('show', () => mQuery('#flashes').addClass('alert-offset')); this.editor.on('hide', () => mQuery('#flashes').removeClass('alert-offset')); } /** * Initialize the grapesjs build in the * correct mode */ initGrapesJS(object) { // grapesjs-custom-plugins: add globally defined mautic-grapesjs-plugins using name as pluginId for the plugin-function if (window.MauticGrapesJsPlugins) { window.MauticGrapesJsPlugins.forEach((item) => { if (!item.name) { console.warn('A name is required for Mautic-GrapesJs plugins in window.MauticGrapesJsPlugins. Registration skipped!'); return; } if (typeof item.plugin !== 'function') { console.warn('The Mautic-GrapesJs plugin must be a function in window.MauticGrapesJsPlugins. Registration skipped!'); return; } grapesjs.plugins.add(item.name, item.plugin); }); } // disable mautic global shortcuts Mousetrap.reset(); if (object === 'page') { this.editor = this.initPage(); } else if (object === 'emailform') { if (MjmlService.getOriginalContentMjml()) { this.editor = this.initEmailMjml(); } else { this.editor = this.initEmailHtml(); } } else { throw Error(`Not supported builder type: ${object}`); } // add code mode button // @todo: only show button if configured: sourceEdit: 1, const codeModeButton = new CodeModeButton(this.editor); codeModeButton.addCommand(); codeModeButton.addButton(); this.storageService = new StorageService(this.editor, object); this.overrideCustomRteDisable(); this.setListeners(); } static getMauticConf(mode) { return { mode, }; } static getCkeConf(tokenCallback) { const ckEditorToolbarOptions = ['undo', 'redo', '|', 'bold','italic', 'underline','strikethrough', '|', 'fontSize','fontFamily','fontColor','fontBackgroundColor', '|' ,'alignment','outdent', 'indent', '|', 'blockQuote', 'insertTable', '|', 'bulletedList','numberedList', '|', 'link', '|', 'TokenPlugin']; return { ckeditor_module: `${mauticBaseUrl}assets/ckeditor/build/ckeditor.js`, options: Mautic.GetCkEditorConfigOptions(ckEditorToolbarOptions, tokenCallback) }; } /** * Initialize the builder in the landingapge mode */ initPage() { // Launch GrapesJS with body part this.editor = grapesjs.init({ clearOnRender: true, container: '.builder-panel', components: contentService.getOriginalContentHtml().body.innerHTML, height: '100%', canvas: { styles: contentService.getStyles(), }, storageManager: false, // https://grapesjs.com/docs/modules/Storage.html#basic-configuration assetManager: this.getAssetManagerConf(), styleManager: { clearProperties: true, // Temp fix https://github.com/artf/grapesjs-preset-webpage/issues/27 }, plugins: [ // partially copied from: https://github.com/GrapesJS/grapesjs/blob/gh-pages/demo.html grapesjswebpage, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor5', grapesjsblocksbasic, grapesjscomponentcountdown, grapesjsnavbar, grapesjscustomcode, grapesjstouch, grapesjspostcss, grapesjstuiimageeditor, grapesjsstylebg, ...BuilderService.getPluginNames('page'), // grapesjs-custom-plugins: load custom plugins by their name ], pluginsOpts: { [grapesjswebpage]: { formsOpts: false, useCustomTheme: false, }, grapesjsmautic: BuilderService.getMauticConf('page-html'), 'gjs-plugin-ckeditor5': BuilderService.getCkeConf('page:getBuilderTokens'), ...BuilderService.getPluginOptions('page'), // grapesjs-custom-plugins: add the plugin-options }, }); this.moveBlocksPage(); return this.editor; } initEmailMjml() { const components = MjmlService.getOriginalContentMjml(); // validate MjmlService.mjmlToHtml(components); const styles = [ `${mauticBaseUrl}plugins/GrapesJsBuilderBundle/Assets/library/js/grapesjs-editor.css` ]; this.editor = grapesjs.init({ selectorManager: { componentFirst: true, }, avoidInlineStyle: false, // TEMP: fixes issue with disappearing inline styles forceClass: false, // create new styles if there are some already on the element: https://github.com/GrapesJS/grapesjs/issues/1531 clearOnRender: true, container: '.builder-panel', height: '100%', canvas: { styles, }, domComponents: { // disable all except link components disableTextInnerChilds: (child) => !child.is('link'), // https://github.com/GrapesJS/grapesjs/releases/tag/v0.21.2 }, storageManager: false, assetManager: this.getAssetManagerConf(), plugins: [grapesjsmjml, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor5', ...BuilderService.getPluginNames('email-mjml')], pluginsOpts: { [grapesjsmjml]: { hideSelector: false, custom: false, useCustomTheme: false, }, grapesjsmautic: BuilderService.getMauticConf('email-mjml'), 'gjs-plugin-ckeditor5': BuilderService.getCkeConf('email:getBuilderTokens'), ...BuilderService.getPluginOptions('email-mjml'), }, }); this.unsetComponentVoidTypes(this.editor); this.editor.setComponents(components); // Reinitialize the content after parsing MJML. // This can be removed once the issue with self-closing tags is resolved in grapesjs-mjml. // See: https://github.com/GrapesJS/mjml/issues/149 const parsedContent = MjmlService.getEditorMjmlContent(this.editor); this.editor.setComponents(parsedContent); this.editor.BlockManager.get('mj-button').set({ content: 'Button', }); return this.editor; } unsetComponentVoidTypes(editor) { // Support for self-closing components is temporarily disabled due to parsing issues with mjml tags. // Browsers only recognize explicit self-closing tags like and
, leading to rendering problems. // This can be reverted once the issue with self-closing tags is resolved in grapesjs-mjml. // See: https://github.com/GrapesJS/mjml/issues/149 const voidTypes = ['mj-image', 'mj-divider', 'mj-font']; voidTypes.forEach(function(component) { editor.DomComponents.addType(component, { model: { defaults: { void: false }, toHTML() { const tag = this.get('tagName'); const attr = this.getAttrToHTML(); const content = this.get('content'); let strAttr = ''; for (let prop in attr) { const val = attr[prop]; const hasValue = typeof val !== 'undefined' && val !== ''; strAttr += hasValue ? ` ${prop}="${val}"` : ''; } let html = `<${tag}${strAttr}>${content}`; // Add the components after the closing tag const componentsHtml = this.get('components') .map(model => model.toHTML()) .join(''); return html + componentsHtml; }, } }); }); } initEmailHtml() { const components = contentService.getOriginalContentHtml().body.innerHTML; if (!components) { throw new Error('no components'); } const styles = [ `${mauticBaseUrl}plugins/GrapesJsBuilderBundle/Assets/library/js/grapesjs-editor.css` ]; // Launch GrapesJS with body part this.editor = grapesjs.init({ clearOnRender: true, container: '.builder-panel', components, height: '100%', canvas: { styles, }, storageManager: false, assetManager: this.getAssetManagerConf(), plugins: [grapesjsnewsletter, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor5', ...BuilderService.getPluginNames('email-html')], pluginsOpts: { grapesjsnewsletter: { useCustomTheme: false, }, grapesjsmautic: BuilderService.getMauticConf('email-html'), 'gjs-plugin-ckeditor5': BuilderService.getCkeConf('email:getBuilderTokens'), ...BuilderService.getPluginOptions('email-html'), }, }); // add a Mautic custom block Button this.editor.BlockManager.get('button').set({ content: '\n' + 'Button\n' + '', }); return this.editor; } /** * Return the names of dynamically added plugins * @param context * @returns string[] */ static getPluginNames(context) { let plugins = []; if (window.MauticGrapesJsPlugins) { window.MauticGrapesJsPlugins.forEach((item) => { if (item.name) { if (!item.context || !Array.isArray(item.context) || item.context.length === 0) { // if no context is given, the plugin is always added plugins.push(item.name); } else { // check if the plugin should be added for the current editor context item.context.forEach((pluginContext) => { if (pluginContext === context) { plugins.push(item.name); } }) } } }); } return plugins; } /** * Return the options of dynamically added plugins * @param context * @returns object[] */ static getPluginOptions(context) { let pluginOptions = {}; if (window.MauticGrapesJsPlugins) { window.MauticGrapesJsPlugins.forEach((item) => { if (!item.context || !Array.isArray(item.context) || item.context.length === 0) { // if no context is given, the plugin is always added pluginOptions[item.name] = item.pluginOptions ?? {}; } else { // check if the plugin should be added for the current editor context item.context.forEach((pluginContext) => { if (pluginContext === context) { pluginOptions[item.name] = item.pluginOptions ?? {}; } }) } }); } return pluginOptions; } /** * Manage button loading indicator * * @param activate - true or false */ static setupButtonLoadingIndicator(activate) { const builderButton = mQuery('.btn-builder'); const saveButton = mQuery('.btn-save'); const applyButton = mQuery('.btn-apply'); if (activate) { Mautic.activateButtonLoadingIndicator(builderButton); Mautic.activateButtonLoadingIndicator(saveButton); Mautic.activateButtonLoadingIndicator(applyButton); } else { Mautic.removeButtonLoadingIndicator(builderButton); Mautic.removeButtonLoadingIndicator(saveButton); Mautic.removeButtonLoadingIndicator(applyButton); } } /** * Configure the Asset Manager for all modes * @link https://grapesjs.com/docs/modules/Assets.html#configuration */ getAssetManagerConf() { return { assets: this.assets, noAssets: Mautic.translate('grapesjsbuilder.assetManager.noAssets'), upload: this.uploadPath, uploadName: 'files', multiUpload: 1, embedAsBase64: false, openAssetsOnDrop: 1, autoAdd: 1, headers: { 'X-CSRF-Token': mauticAjaxCsrf }, // global variable }; } getEditor() { return this.editor; } // https://github.com/artf/grapesjs-mjml/issues/193 overrideCustomRteDisable() { const richTextEditor = this.editor.RichTextEditor; if (!richTextEditor) { console.error('No RichTextEditor found'); return; } if (richTextEditor.customRte) { richTextEditor.customRte.disable = (el, rte) => { el.contentEditable = false; if (rte && rte.focusManager) { rte.focusManager.blur(true); } if (rte && typeof rte.destroy == 'function') { rte.destroy(); } }; } } /** * Move the blocks and categories in the sidebar */ moveBlocksPage() { const blocks = this.editor.BlockManager.getAll(); blocks.map(block => { // columns go into a new category, at the top if(block.attributes.id.indexOf('column') !== -1) { this.editor.BlockManager.get(block.attributes.id).set('category', { label:"Sections", order: -1 }); } // 'Blocks' category goes after 'Basic' if(block.attributes.category === 'Basic') { this.editor.BlockManager.get(block.attributes.id).set('category', { label:"Basic", order: -1 }); } }); } /** * Generate assets list from GrapesJs */ // getAssetsList() { // const assetManager = this.editor.AssetManager; // const assets = assetManager.getAll(); // const assetsList = []; // assets.forEach((asset) => { // if (asset.get('type') === 'image') { // assetsList.push({ // src: asset.get('src'), // width: asset.get('width'), // height: asset.get('height'), // }); // } else { // assetsList.push(asset.get('src')); // } // }); // return assetsList; // } }