Spaces:
No application file
No application file
| 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: '<mj-button href="https://">Button</mj-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 <img /> and <br />, 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}</${tag}>`; | |
| // 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: | |
| '<a href="#" target="_blank" style="display:inline-block;text-decoration:none;border-color:#4e5d9d;border-width: 10px 20px;border-style:solid; text-decoration: none; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; background-color: #4e5d9d; display: inline-block;font-size: 16px; color: #ffffff; ">\n' + | |
| 'Button\n' + | |
| '</a>', | |
| }); | |
| 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; | |
| // } | |
| } | |