Initial commit: CloudOps infrastructure platform
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1 @@
|
||||
.code-panel{text-align:left;font-size:1rem;height:100%;display:flex;flex-direction:column}.code-panel section{flex:1}.code-panel section .codepanel-separator{display:flex;justify-content:space-between;padding-left:.6rem;padding-right:.6rem}.code-panel section .codepanel-label{line-height:20px;font-size:13px;color:#aaa;user-select:none;text-transform:uppercase}.code-panel section button{background-color:#d6d6d6}.gutter{cursor:ns-resize;position:relative;background-color:rgba(0,0,0,0.2)}.gutter:after{content:'';display:block;height:8px;width:100%;position:absolute;top:-3px;z-index:150}.code-panel .CodeMirror{height:calc(100% - 20px)}.gjs-pn-views{border-left:1px solid rgba(0,0,0,0.2);border-bottom:0}.gjs-pn-views-container{box-shadow:initial;top:40px;padding-top:0;height:calc(100% - 40px)}.gjs-pn-views-container,.gjs-cv-canvas{transition:width .3s ease-in-out}
|
||||
@@ -0,0 +1,85 @@
|
||||
export default class AssetService {
|
||||
constructor() {
|
||||
this.uploadPath = '/s/grapesjsbuilder/upload';
|
||||
this.deletePath = '/s/grapesjsbuilder/delete';
|
||||
this.assetsPath = '/s/grapesjsbuilder/media';
|
||||
this.assetsPage = 1;
|
||||
this.totalPages = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for uploading assets
|
||||
* @returns {string} The upload path
|
||||
*/
|
||||
getUploadPath() {
|
||||
return this.uploadPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for deleting assets
|
||||
* @returns {string} The delete path
|
||||
*/
|
||||
getDeletePath() {
|
||||
return this.deletePath;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get assets path with current page
|
||||
* @returns {string}
|
||||
*/
|
||||
getAssetsPath() {
|
||||
return `${this.assetsPath}?page=${this.assetsPage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch assets from the server
|
||||
* @param {string} assetsPath
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async fetchAssets(assetsPath) {
|
||||
try {
|
||||
const response = await fetch(assetsPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.totalPages !== undefined) {
|
||||
this.totalPages = result.totalPages;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assets for the first page
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getAssetsXhr() {
|
||||
this.assetsPage = 1;
|
||||
return this.fetchAssets(this.getAssetsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assets for the next page
|
||||
* @returns {Promise<Object>|null}
|
||||
*/
|
||||
async getAssetsNextPageXhr() {
|
||||
if (this.hasLoadedAllAssets()) {
|
||||
return null;
|
||||
}
|
||||
this.assetsPage++;
|
||||
return this.fetchAssets(this.getAssetsPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all assets have been loaded
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasLoadedAllAssets() {
|
||||
return this.totalPages !== null && this.assetsPage >= this.totalPages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import AssetService from './asset.service';
|
||||
import BuilderService from './builder.service';
|
||||
|
||||
// all css get combined into one builder.css and automatically loaded via js/parcel
|
||||
import 'grapesjs/dist/css/grapes.min.css';
|
||||
import './grapesjs-custom.css';
|
||||
|
||||
/**
|
||||
* Launch builder
|
||||
*
|
||||
* @param formName
|
||||
*/
|
||||
function launchBuilderGrapesjs(formName) {
|
||||
if (useBuilderForCodeMode() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mautic.showChangeThemeWarning = true;
|
||||
|
||||
// Prepare HTML
|
||||
mQuery('html').css('font-size', '100%');
|
||||
mQuery('body').css('overflow-y', 'hidden');
|
||||
mQuery('.builder-panel').css('padding', 0);
|
||||
mQuery('.builder-panel').css('display', 'block');
|
||||
const $builder = mQuery('.builder');
|
||||
$builder.addClass('builder-active').removeClass('hide');
|
||||
|
||||
const assetService = new AssetService();
|
||||
const builder = new BuilderService(assetService);
|
||||
// Initialize GrapesJS
|
||||
builder.initGrapesJS(formName);
|
||||
|
||||
// trigger show event on DOM element
|
||||
$builder.trigger('builder:show', [builder.editor])
|
||||
// trigger show event on editor instance
|
||||
builder.editor.trigger('show');
|
||||
|
||||
// Load and add assets
|
||||
(async () => {
|
||||
try {
|
||||
const result = await assetService.getAssetsXhr();
|
||||
builder.editor.AssetManager.add(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading initial assets:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* The user acknowledges the risk before editing an email or landing page created in Code Mode in the Builder
|
||||
*/
|
||||
function useBuilderForCodeMode() {
|
||||
const theme = mQuery('.theme-selected').find('[data-theme]').attr('data-theme');
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
|
||||
if (isCodeMode) {
|
||||
if (confirm(Mautic.translate('grapesjsbuilder.builder.warning.code_mode')) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme's HTML
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function setThemeHtml(theme) {
|
||||
BuilderService.setupButtonLoadingIndicator(true);
|
||||
// Load template and fill field
|
||||
mQuery.ajax({
|
||||
url: mQuery('#builder_url').val(),
|
||||
data: `template=${theme}`,
|
||||
dataType: 'json',
|
||||
success(response) {
|
||||
const textareaHtml = mQuery('textarea.builder-html');
|
||||
const textareaMjml = mQuery('textarea.builder-mjml');
|
||||
|
||||
textareaHtml.val(response.templateHtml);
|
||||
|
||||
if (typeof textareaMjml !== 'undefined') {
|
||||
textareaMjml.val(response.templateMjml);
|
||||
}
|
||||
|
||||
// If MJML template, generate HTML before save
|
||||
if (!textareaHtml.val().length && textareaMjml.val().length) {
|
||||
const assetService = new AssetService();
|
||||
const builder = new BuilderService(assetService);
|
||||
|
||||
textareaHtml.val(builder.mjmlToHtml(response.templateMjml));
|
||||
}
|
||||
},
|
||||
error(request, textStatus) {
|
||||
console.log(`setThemeHtml - Request failed: ${textStatus}`);
|
||||
},
|
||||
complete() {
|
||||
BuilderService.setupButtonLoadingIndicator(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The builder button to launch GrapesJS will be disabled when the code mode theme is selected
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function switchBuilderButton(theme) {
|
||||
const builderButton = mQuery('.btn-builder');
|
||||
const mEmailBuilderButton = mQuery('#emailform_buttons_builder_toolbar_mobile');
|
||||
const mPageBuilderButton = mQuery('#page_buttons_builder_toolbar_mobile');
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
|
||||
builderButton.attr('disabled', isCodeMode);
|
||||
|
||||
if (isCodeMode) {
|
||||
mPageBuilderButton.addClass('link-is-disabled');
|
||||
mEmailBuilderButton.addClass('link-is-disabled');
|
||||
|
||||
mPageBuilderButton.parent().addClass('is-not-allowed');
|
||||
mEmailBuilderButton.parent().addClass('is-not-allowed');
|
||||
} else {
|
||||
mPageBuilderButton.removeClass('link-is-disabled');
|
||||
mEmailBuilderButton.removeClass('link-is-disabled');
|
||||
|
||||
mPageBuilderButton.parent().removeClass('is-not-allowed');
|
||||
mEmailBuilderButton.parent().removeClass('is-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The textarea with the HTML source will be displayed if the code mode theme is selected
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
function switchCustomHtml(theme) {
|
||||
const customHtmlRow = mQuery('#custom-html-row');
|
||||
const isPageMode = mQuery('[name="page"]').length !== 0;
|
||||
const isCodeMode = theme === 'mautic_code_mode';
|
||||
const advancedTab = isPageMode ? mQuery('#advanced-tab') : null;
|
||||
|
||||
if (isCodeMode === true) {
|
||||
customHtmlRow.removeClass('hidden');
|
||||
isPageMode && advancedTab.removeClass('hidden');
|
||||
} else {
|
||||
customHtmlRow.addClass('hidden');
|
||||
isPageMode && advancedTab.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize original Mautic theme selection with grapejs specific modifications
|
||||
*/
|
||||
function initSelectThemeGrapesjs(parentInitSelectTheme) {
|
||||
function childInitSelectTheme(themeField) {
|
||||
const builderUrl = mQuery('#builder_url');
|
||||
let url;
|
||||
|
||||
switchBuilderButton(themeField.val());
|
||||
switchCustomHtml(themeField.val());
|
||||
|
||||
// Replace Mautic URL by plugin URL
|
||||
if (builderUrl.length) {
|
||||
if (builderUrl.val().indexOf('pages') !== -1) {
|
||||
url = builderUrl.val().replace('s/pages/builder', 's/grapesjsbuilder/page');
|
||||
} else {
|
||||
url = builderUrl.val().replace('s/emails/builder', 's/grapesjsbuilder/email');
|
||||
}
|
||||
|
||||
builderUrl.val(url);
|
||||
}
|
||||
|
||||
// Launch original Mautic.initSelectTheme function
|
||||
parentInitSelectTheme(themeField);
|
||||
|
||||
mQuery('[data-theme]').click((event) => {
|
||||
const target = mQuery(event.target);
|
||||
const theme = target.closest('[data-theme]').attr('data-theme');
|
||||
|
||||
switchBuilderButton(theme);
|
||||
switchCustomHtml(theme);
|
||||
});
|
||||
}
|
||||
return childInitSelectTheme;
|
||||
}
|
||||
|
||||
Mautic.launchBuilder = launchBuilderGrapesjs;
|
||||
Mautic.initSelectTheme = initSelectThemeGrapesjs(Mautic.initSelectTheme);
|
||||
Mautic.setThemeHtml = setThemeHtml;
|
||||
@@ -0,0 +1,550 @@
|
||||
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 grapesjsckeditor from './plugins/grapesjs.ckeditor';
|
||||
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 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 CompCopyPaste from './commands/compCopyPaste';
|
||||
import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service';
|
||||
|
||||
export default class BuilderService {
|
||||
editor;
|
||||
|
||||
storageService;
|
||||
|
||||
assetService;
|
||||
|
||||
/**
|
||||
* @param {AssetService} assetService
|
||||
*/
|
||||
constructor(assetService) {
|
||||
this.assetService = assetService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.assetService.getDeletePath(),
|
||||
data: { filename: response.getFilename() },
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.on('asset:upload:error', (error) => {
|
||||
Mautic.setFlashes(Mautic.addErrorFlashMessage(error));
|
||||
});
|
||||
|
||||
this.editor.on('asset:open', () => {
|
||||
const editor = this.editor;
|
||||
const assetsService = this.assetService;
|
||||
const assetsContainer = document.querySelector('.gjs-am-assets');
|
||||
const $assetsSpinner = document.createElement('div');
|
||||
$assetsSpinner.className = 'gjs-assets-spinner';
|
||||
$assetsSpinner.innerHTML = '<i class="ri-loader-3-line ri-spin"></i>';
|
||||
|
||||
if (assetsContainer) {
|
||||
let isLoading = false;
|
||||
|
||||
const loadNextPage = async () => {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
assetsContainer.appendChild($assetsSpinner);
|
||||
|
||||
try {
|
||||
const result = await assetsService.getAssetsNextPageXhr();
|
||||
if (result) {
|
||||
const assetManager = editor.AssetManager;
|
||||
const currentAssets = assetManager.getAll().models;
|
||||
const newAssets = result.data;
|
||||
|
||||
// Combine current assets with new assets
|
||||
const combinedAssets = [...currentAssets, ...newAssets];
|
||||
|
||||
// Reset the entire collection with combined assets
|
||||
assetManager.getAll().reset(combinedAssets);
|
||||
assetManager.render();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading next page of assets:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
assetsContainer.addEventListener('scroll', function() {
|
||||
const hasScrolledToBottom = this.scrollTop + this.clientHeight >= this.scrollHeight - 5;
|
||||
if (hasScrolledToBottom && !assetsService.hasLoadedAllAssets()) {
|
||||
loadNextPage();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Element with class "gjs-am-assets" not found');
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* Add command that will allow users
|
||||
* to copy paste component across tabs
|
||||
*/
|
||||
new CompCopyPaste(this.editor).addCommand();
|
||||
|
||||
this.storageService = new StorageService(this.editor, object);
|
||||
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 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,
|
||||
grapesjsckeditor,
|
||||
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'),
|
||||
[grapesjsckeditor]: BuilderService.getCkeConf('page:getBuilderTokens'),
|
||||
...BuilderService.getPluginOptions('page'), // grapesjs-custom-plugins: add the plugin-options
|
||||
},
|
||||
});
|
||||
|
||||
this.moveBlocksPage();
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
mjmlToHtml(mjml) {
|
||||
const converted = MjmlService.mjmlToHtml(mjml);
|
||||
|
||||
if (0 === converted.errors.length) {
|
||||
return converted.html;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
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, grapesjsckeditor, ...BuilderService.getPluginNames('email-mjml')],
|
||||
pluginsOpts: {
|
||||
[grapesjsmjml]: {
|
||||
hideSelector: false,
|
||||
custom: false,
|
||||
useCustomTheme: false,
|
||||
},
|
||||
grapesjsmautic: BuilderService.getMauticConf('email-mjml'),
|
||||
[grapesjsckeditor]: 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>',
|
||||
});
|
||||
|
||||
this.removeSelectedElementsEmailMjml();
|
||||
|
||||
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', 'mj-spacer'];
|
||||
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, grapesjsckeditor, ...BuilderService.getPluginNames('email-html')],
|
||||
pluginsOpts: {
|
||||
grapesjsnewsletter: {
|
||||
useCustomTheme: false,
|
||||
},
|
||||
grapesjsmautic: BuilderService.getMauticConf('email-html'),
|
||||
[grapesjsckeditor]: 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: [],
|
||||
noAssets: Mautic.translate('grapesjsbuilder.assetManager.noAssets'),
|
||||
upload: this.assetService.getUploadPath(),
|
||||
uploadName: 'files',
|
||||
multiUpload: 1,
|
||||
embedAsBase64: false,
|
||||
openAssetsOnDrop: 1,
|
||||
autoAdd: 1,
|
||||
headers: { 'X-CSRF-Token': mauticAjaxCsrf }, // global variable
|
||||
};
|
||||
}
|
||||
|
||||
getEditor() {
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeSelectedElementsEmailMjml() {
|
||||
|
||||
// Remove the RAW block (it's just not usable)
|
||||
const rawblock = this.editor.BlockManager.get('mj-raw');
|
||||
|
||||
if (rawblock !== null) {
|
||||
this.editor.BlockManager.remove(rawblock);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// import ContentService from '../../../../../../../grapesjs-preset-mautic/src/content.service';
|
||||
import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service';
|
||||
import ContentService from 'grapesjs-preset-mautic/dist/content.service';
|
||||
|
||||
class CodeEditor {
|
||||
editor;
|
||||
|
||||
opts;
|
||||
|
||||
codeEditor;
|
||||
|
||||
codePopup;
|
||||
|
||||
constructor(editor, opts = {}) {
|
||||
this.editor = editor;
|
||||
this.opts = opts;
|
||||
|
||||
this.codeEditor = this.buildCodeEditor();
|
||||
this.codePopup = this.buildCodePopup();
|
||||
}
|
||||
|
||||
// Build codeEditor (CodeMirror instance)
|
||||
buildCodeEditor() {
|
||||
const codeEditor = this.editor.CodeManager.getViewer('CodeMirror').clone();
|
||||
|
||||
codeEditor.set({
|
||||
codeName: 'htmlmixed',
|
||||
readOnly: false,
|
||||
theme: 'hopscotch',
|
||||
autoBeautify: true,
|
||||
autoCloseTags: true,
|
||||
autoCloseBrackets: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true,
|
||||
smartIndent: true,
|
||||
indentWithTabs: true,
|
||||
});
|
||||
|
||||
return codeEditor;
|
||||
}
|
||||
|
||||
// Build popup content, codeEditor area and buttons
|
||||
buildCodePopup() {
|
||||
const cfg = this.editor.getConfig();
|
||||
|
||||
const codePopup = document.createElement('div');
|
||||
const btnEdit = document.createElement('button');
|
||||
const btnCancel = document.createElement('button');
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
btnEdit.innerHTML = Mautic.translate('grapesjsbuilder.sourceEditBtnLabel');
|
||||
btnEdit.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-edit`;
|
||||
btnEdit.onclick = this.updateCode.bind(this);
|
||||
|
||||
btnCancel.innerHTML = Mautic.translate('grapesjsbuilder.sourceCancelBtnLabel');
|
||||
btnCancel.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-cancel`;
|
||||
btnCancel.onclick = this.cancelCode.bind(this);
|
||||
|
||||
codePopup.appendChild(textarea);
|
||||
codePopup.appendChild(btnEdit);
|
||||
codePopup.appendChild(btnCancel);
|
||||
|
||||
this.codeEditor.init(textarea);
|
||||
|
||||
return codePopup;
|
||||
}
|
||||
|
||||
// Load content and show popup
|
||||
showCodePopup(editor) {
|
||||
this.updateEditorContents();
|
||||
// this.codeEditor.editor.refresh();
|
||||
// editor.Modal.setContent('');
|
||||
editor.Modal.setContent(this.codePopup);
|
||||
editor.Modal.setTitle(Mautic.translate('grapesjsbuilder.sourceEditModalTitle'));
|
||||
editor.Modal.open();
|
||||
|
||||
editor.Modal.onceClose(() => editor.stopCommand('preset-mautic:code-edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the main editors canvas content with the
|
||||
* content from modals editor.
|
||||
* @todo show validation results in UI
|
||||
*/
|
||||
updateCode() {
|
||||
const code = this.codeEditor.editor.getValue();
|
||||
// validate MJML code
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
MjmlService.mjmlToHtml(code);
|
||||
}
|
||||
|
||||
try {
|
||||
// delete canvas and set new content
|
||||
this.editor.DomComponents.getWrapper().set('content', '');
|
||||
this.editor.setComponents(code.trim())
|
||||
|
||||
// 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.Modal.close();
|
||||
} catch (e) {
|
||||
window.alert(`${Mautic.translate('grapesjsbuilder.sourceSyntaxError')} \n${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Close popup
|
||||
cancelCode() {
|
||||
this.editor.Modal.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content to be edited in the popup editor
|
||||
*/
|
||||
updateEditorContents() {
|
||||
// Check if MJML plugin is on
|
||||
let content;
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
content = MjmlService.getEditorMjmlContent(this.editor);
|
||||
} else {
|
||||
content = ContentService.getEditorHtmlContent(this.editor);
|
||||
}
|
||||
this.codeEditor.setContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
@@ -0,0 +1,35 @@
|
||||
import CodeModeCommand from './codeMode.command';
|
||||
|
||||
export default class CodeModeButton {
|
||||
editor;
|
||||
|
||||
/**
|
||||
* Add close button with save for Mautic
|
||||
*/
|
||||
constructor(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
addButton() {
|
||||
this.editor.Panels.addButton('options', [
|
||||
{
|
||||
id: 'code-edit',
|
||||
className: 'ri-edit-line',
|
||||
attributes: {
|
||||
title: Mautic.translate('grapesjsbuilder.sourceEditModalTitle'),
|
||||
},
|
||||
command: CodeModeCommand.name,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
addCommand() {
|
||||
this.editor.Commands.add(CodeModeCommand.name, {
|
||||
run: CodeModeCommand.launchCodeEditorModal,
|
||||
stop: CodeModeCommand.stopCodeEditorModal,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import CodeEditor from './codeEditor';
|
||||
|
||||
export default class CodeModeCommand {
|
||||
/**
|
||||
* The command to run on button click
|
||||
*/
|
||||
static name = 'preset-mautic:code-edit';
|
||||
|
||||
static codeEditor;
|
||||
|
||||
static launchCodeEditorModal(editor, sender, opts) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
|
||||
CodeModeCommand.codeEditor = new CodeEditor(editor, opts);
|
||||
|
||||
if (sender) {
|
||||
sender.set('active', 0);
|
||||
}
|
||||
|
||||
CodeModeCommand.codeEditor.showCodePopup(editor);
|
||||
|
||||
// Transform DC Component to token
|
||||
editor.runCommand('preset-mautic:dynamic-content-components-to-tokens');
|
||||
}
|
||||
|
||||
static stopCodeEditorModal(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
// Transform Token to Components
|
||||
editor.runCommand('preset-mautic:update-dc-components-from-dc-store');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// copied from: https://github.com/GrapesJS/grapesjs/issues/1855
|
||||
|
||||
export default class CompCopyPaste {
|
||||
static storage_key = 'preset-mautic:grapesjs-clipboard';
|
||||
editor;
|
||||
|
||||
// COPY PASTE COMPONENTS/STYLE BETWEEN PAGES
|
||||
getStyles(components) {
|
||||
// recurse down through components and store styles in temp attribute
|
||||
components.forEach((component) => {
|
||||
const recurse = (comp) => {
|
||||
// if component has any styling
|
||||
if (Object.keys(comp.getStyle()).length !== 0) comp.attributes.savedStyle = comp.getStyle();
|
||||
if (comp.get('components').length) {
|
||||
comp.get('components').forEach((child) => {
|
||||
recurse(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
recurse(component);
|
||||
});
|
||||
return components;
|
||||
}
|
||||
|
||||
setStyles(component) {
|
||||
// recurse down and re-apply style back to components
|
||||
const recurse = (comp) => {
|
||||
if ('savedStyle' in comp.attributes) {
|
||||
comp.setStyle(comp.attributes.savedStyle);
|
||||
delete comp.attributes.savedStyle;
|
||||
}
|
||||
if (comp.attributes.components.length) {
|
||||
comp.attributes.components.forEach((child) => {
|
||||
recurse(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
recurse(component);
|
||||
}
|
||||
|
||||
newCopy(selected) {
|
||||
window.localStorage.setItem(this.storage_key, JSON.stringify(selected));
|
||||
}
|
||||
|
||||
newPaste(selected) {
|
||||
let components = JSON.parse(window.localStorage.getItem(this.storage_key));
|
||||
if (components) {
|
||||
if (selected && selected.attributes.type !== 'wrapper') {
|
||||
const index = selected.index();
|
||||
// Invert the order so last item gets added first and gets pushed down as others get added.
|
||||
components.reverse();
|
||||
const currentSelection = selected.collection;
|
||||
components.forEach((comp) => {
|
||||
if (currentSelection) {
|
||||
const added = currentSelection.add(comp, { at: index + 1 });
|
||||
editor.trigger('component:paste', added);
|
||||
this.setStyles(added);
|
||||
}
|
||||
});
|
||||
selected.emitUpdate();
|
||||
} else {
|
||||
components = editor.addComponents(components);
|
||||
components.forEach((comp) => {
|
||||
this.setStyles(comp);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(editor) {
|
||||
if (!editor) {
|
||||
throw new Error('no editor');
|
||||
}
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
addCommand() {
|
||||
this.editor.Commands.add('core:copy', (ed) => {
|
||||
const selected = this.getStyles([...ed.getSelectedAll()]);
|
||||
let filteredSelected = selected.filter((item) => item.attributes.copyable == true);
|
||||
if (filteredSelected.length) {
|
||||
this.newCopy(filteredSelected);
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.Commands.add('core:paste', (ed) => {
|
||||
const selected = ed.getSelected();
|
||||
this.newPaste(selected);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,375 @@
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* GrapesJS temporary fix #2490 */
|
||||
.gjs-clm-tag-status,
|
||||
.gjs-clm-tag-close {
|
||||
}
|
||||
.gjs-clm-tags-btn {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
/* Fix hidden scroll */
|
||||
.gjs-pn-views-container {
|
||||
height: auto;
|
||||
padding: 0 0 0;
|
||||
top: 40px;
|
||||
bottom: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.gjs-btn-code-edit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* GrapesJS FileManager custom CSS */
|
||||
.gjs-btn-code-cancel {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.gjs-am-assets-cont {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.gjs-am-assets {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.gjs-am-asset {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader > form #gjs-am-uploadFile {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.gjs-am-file-uploader #gjs-am-title {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.gjs-am-preview {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.gjs-am-preview-cont {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.gjs-am-meta {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.gjs-pn-commands .gjs-pn-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* GrapesJS Colors / Theme */
|
||||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
}
|
||||
|
||||
.gjs-block-category .gjs-title,
|
||||
.gjs-category-title,
|
||||
.gjs-clm-tags .gjs-sm-title,
|
||||
.gjs-layer-title,
|
||||
.gjs-sm-sector .gjs-sm-title {
|
||||
font-weight: lighter;
|
||||
background-color: rgba(0,0,0,.1);
|
||||
letter-spacing: 1px;
|
||||
padding: 9px 10px 9px 20px;
|
||||
border-bottom: 1px solid rgba(0,0,0,.25);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gjs-sm-properties {
|
||||
font-size: .75rem;
|
||||
padding: 10px 5px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.gjs-clm-tags .gjs-clm-tag {
|
||||
border: 1px solid #707070;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-field {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-btnt.gjs-pn-active,
|
||||
.gjs-pn-btn.gjs-pn-active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gjs-import-label,
|
||||
.gjs-export-label {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog .gjs-btn-import {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 3px;
|
||||
height: 450px;
|
||||
font-family: sans-serif, monospace;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* GrapesJS Extra */
|
||||
#gjs-pn-views-container.gjs-pn-panel {
|
||||
padding: 39px 0 0;
|
||||
}
|
||||
|
||||
#gjs-pn-views.gjs-pn-panel {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-btn {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
padding: 10px;
|
||||
width: 25%;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#gjs-pn-views .gjs-pn-active {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#gjs-pn-devices-c {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#gjs-pn-options {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.gjs-sm-composite .gjs-sm-properties {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#gjs-sm-border-top-left-radius,
|
||||
#gjs-sm-border-top-right-radius,
|
||||
#gjs-sm-border-bottom-left-radius,
|
||||
#gjs-sm-border-bottom-right-radius,
|
||||
#gjs-sm-margin-top,
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-top,
|
||||
#gjs-sm-padding-bottom,
|
||||
#gjs-sm-padding-right,
|
||||
#gjs-sm-padding-left {
|
||||
}
|
||||
|
||||
#gjs-sm-border-width,
|
||||
#gjs-sm-border-style,
|
||||
#gjs-sm-border-color {
|
||||
flex: 999 1 80px;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-left,
|
||||
#gjs-sm-padding-left {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-right,
|
||||
#gjs-sm-padding-right {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
#gjs-sm-margin-bottom,
|
||||
#gjs-sm-padding-bottom {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.gjs-field-radio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gjs-field-radio #gjs-sm-input-holder {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gjs-radio-item {
|
||||
flex: 1 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property.gjs-sm-list {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.gjs-mdl-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active {
|
||||
background-color: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
.gjs-f-divider::before {
|
||||
content: 'D';
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog-sm {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-property {
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gjs-mdl-dialog form .gjs-sm-label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#gjs-clm-status-c {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f92929;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.is-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.link-is-disabled {
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* overrides for the icons now that they are SVGs */
|
||||
|
||||
.gjs-pn-panel .gjs-block__media svg {
|
||||
max-height: 48px;
|
||||
}
|
||||
|
||||
/* Newsletter preset theme */
|
||||
|
||||
.gjs-one-bg {
|
||||
background-color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-one-color {
|
||||
color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-one-color-h:hover {
|
||||
color: #373d49;
|
||||
}
|
||||
|
||||
.gjs-two-bg{
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-two-color {
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-two-color-h:hover {
|
||||
color: #dae5e6;
|
||||
}
|
||||
|
||||
.gjs-three-bg {
|
||||
background-color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-three-color {
|
||||
color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-three-color-h:hover {
|
||||
color: #4c9790;
|
||||
}
|
||||
|
||||
.gjs-four-bg {
|
||||
background-color: #35d7bb;
|
||||
}
|
||||
|
||||
.gjs-four-color {
|
||||
color: #35d7bb;
|
||||
}
|
||||
|
||||
.gjs-four-color-h:hover {
|
||||
color: #35d7bb;
|
||||
}
|
||||
|
||||
/* revert colors for nested panels */
|
||||
.gjs-mdl-dialog .panel {
|
||||
color: var(--gjs-main-color);
|
||||
}
|
||||
|
||||
.gjs-assets-spinner {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.anim-spin {
|
||||
animation: 0.5s linear 0s normal none infinite running spin; }
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
|
||||
/* CKEditor styles */
|
||||
.cke-modal .ck-editor .ck-content {
|
||||
min-height: 400px;
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.cke-modal .gjs-btn-prim {
|
||||
margin: 10px 5px 5px 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
html,
|
||||
body,
|
||||
[data-gjs-type='wrapper'] {
|
||||
height: auto !important;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
export default (editor, opts = {}) => {
|
||||
let ckEditorInstance = null;
|
||||
|
||||
const SIMPLE_EDITING_TYPES = ['mj-button', 'link'];
|
||||
|
||||
editor.on('rte:enable', (view, gjsRte) => {
|
||||
if (!isSimpleEditingEl(view.el)) {
|
||||
openCKEditorModal(gjsRte.el, view);
|
||||
editor.RichTextEditor.hideToolbar();
|
||||
}
|
||||
});
|
||||
|
||||
function isSimpleEditingEl(el) {
|
||||
return el && el.getAttribute && SIMPLE_EDITING_TYPES.includes(el.getAttribute('data-gjs-type'));
|
||||
}
|
||||
|
||||
function openCKEditorModal(el, view) {
|
||||
const ckEditorElementId = `ckeditor-${Date.now()}`;
|
||||
const modal = editor.Modal;
|
||||
|
||||
modal.onceOpen(() => initCKEditor(ckEditorElementId));
|
||||
modal.onceClose(() => destroyCKEditor());
|
||||
modal.config.backdrop = false;
|
||||
modal.open({
|
||||
title: 'Edit',
|
||||
content: `
|
||||
<div id="${ckEditorElementId}">${el.innerHTML}</div>
|
||||
<button type="button" class="gjs-btn-prim" id="gjs-cke-save-btn">Save</button>
|
||||
<button type="button" class="gjs-btn-prim" id="gjs-cke-close-btn">Cancel</button>
|
||||
`,
|
||||
attributes: {
|
||||
class: 'cke-modal'
|
||||
}
|
||||
});
|
||||
|
||||
const { backgroundColor, color } = getRealColors(el);
|
||||
setEditorStyle(color, backgroundColor);
|
||||
|
||||
document.getElementById('gjs-cke-save-btn').onclick = () => saveContent(view, modal);
|
||||
document.getElementById('gjs-cke-close-btn').onclick = () => modal.close();
|
||||
}
|
||||
|
||||
function initCKEditor(elementId) {
|
||||
if (typeof ClassicEditor === 'undefined') {
|
||||
throw new Error('CKEDITOR instance not found');
|
||||
}
|
||||
ClassicEditor.create(document.getElementById(elementId), opts)
|
||||
.then(instance => {
|
||||
ckEditorInstance = instance;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error initializing CKEditor:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function destroyCKEditor() {
|
||||
if (ckEditorInstance) {
|
||||
ckEditorInstance.destroy()
|
||||
.catch(error => {
|
||||
console.error('Error destroying CKEditor instance:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function saveContent(view, modal) {
|
||||
if (ckEditorInstance) {
|
||||
const content = ckEditorInstance.getData();
|
||||
const selectedElement = view.model;
|
||||
const currentContent = selectedElement.get('content');
|
||||
if (currentContent !== content) {
|
||||
// Clear existing components to avoid conflicts
|
||||
selectedElement.components('');
|
||||
// Set the new content
|
||||
selectedElement.set('content', content);
|
||||
}
|
||||
}
|
||||
modal.close();
|
||||
}
|
||||
|
||||
function setEditorStyle(color, backgroundColor) {
|
||||
const STYLE_ID = 'gjs-ckeditor-styles';
|
||||
let styleElement = document.getElementById(STYLE_ID);
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = STYLE_ID;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
styleElement.innerHTML = `
|
||||
.cke-modal .ck-editor .ck-content {
|
||||
background: ${backgroundColor};
|
||||
color: ${color};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function getRealColors(elem, maxDepth = 100) {
|
||||
const transparent = ['rgba(0, 0, 0, 0)', 'transparent'];
|
||||
const defaults = { backgroundColor: 'rgba(0, 0, 0, 0)', color: 'rgb(0, 0, 0)' };
|
||||
|
||||
function getColors(el, depth) {
|
||||
if (!el || depth <= 0) return defaults;
|
||||
try {
|
||||
const style = getComputedStyle(el);
|
||||
const bg = style.backgroundColor;
|
||||
const color = style.color;
|
||||
const result = {};
|
||||
|
||||
if (!transparent.includes(bg)) result.backgroundColor = bg;
|
||||
if (color && !transparent.includes(color)) result.color = color;
|
||||
|
||||
if (result.backgroundColor && result.color) return result;
|
||||
|
||||
const parentColors = getColors(el.parentElement, depth - 1);
|
||||
return {
|
||||
backgroundColor: result.backgroundColor || parentColors.backgroundColor,
|
||||
color: result.color || parentColors.color
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Error computing colors:', error);
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
return getColors(elem, maxDepth);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import MjmlService from "grapesjs-preset-mautic/dist/mjml/mjml.service";
|
||||
import ContentService from 'grapesjs-preset-mautic/dist/content.service';
|
||||
|
||||
export default class StorageService {
|
||||
constructor(editor, mode) {
|
||||
this.editor = editor;
|
||||
this.mode = mode;
|
||||
this.maxStorageItems = 10;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.storageKey = 'gjs-storage';
|
||||
this.restoreMessage = null;
|
||||
const stackItemId = this.getStackItemId();
|
||||
const storageItem = this.getStorageItemById(stackItemId);
|
||||
const editorContent = this.getEditorContent();
|
||||
if (storageItem && editorContent !== storageItem.content) {
|
||||
this.displayRestoreMessage(storageItem);
|
||||
}
|
||||
this.editor.on("update", () => this.handleUpdate());
|
||||
this.addFormSubmitListeners();
|
||||
}
|
||||
|
||||
displayRestoreMessage(storedContent) {
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'alert-growl-buttons';
|
||||
|
||||
const restoreButton = document.createElement('button');
|
||||
restoreButton.innerHTML = '<i class="ri-arrow-go-back-line"></i> ' + Mautic.translate('mautic.core.builder.storage.restore.button')
|
||||
restoreButton.className = 'btn btn-primary btn-sm ml-md';
|
||||
|
||||
const dismissButton = document.createElement('button');
|
||||
dismissButton.innerHTML = Mautic.translate('mautic.core.builder.storage.dismiss.button')
|
||||
dismissButton.className = 'btn btn-ghost btn-sm';
|
||||
buttonContainer.append(restoreButton, dismissButton);
|
||||
|
||||
const formattedDateTime = this.formatDateTime(storedContent.date);
|
||||
const message = Mautic.translate('mautic.core.builder.storage.restore.message', {
|
||||
date: formattedDateTime
|
||||
});
|
||||
const flashMessage = Mautic.addInfoFlashMessage(message);
|
||||
flashMessage.append(buttonContainer);
|
||||
|
||||
const closeButton = flashMessage.querySelector('button.close')
|
||||
|
||||
this.addMessageEventListeners(restoreButton, dismissButton, closeButton);
|
||||
Mautic.setFlashes(flashMessage, false);
|
||||
this.restoreMessage = flashMessage;
|
||||
}
|
||||
|
||||
dismissRestoreMessage() {
|
||||
if (this.restoreMessage instanceof Element) {
|
||||
this.restoreMessage.remove();
|
||||
}
|
||||
this.restoreMessage = null;
|
||||
}
|
||||
|
||||
addMessageEventListeners(restoreButton, dismissButtom, closeButton) {
|
||||
restoreButton.addEventListener('click', (event) => {
|
||||
this.load();
|
||||
this.dismissRestoreMessage();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
dismissButtom.addEventListener('click', (event) => {
|
||||
this.handleUpdate();
|
||||
this.dismissRestoreMessage();
|
||||
this.removeStorageItemById(this.getStackItemId());
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
closeButton.addEventListener('click', () => {
|
||||
this.handleUpdate();
|
||||
});
|
||||
|
||||
this.editor.on('hide', () => this.dismissRestoreMessage());
|
||||
}
|
||||
|
||||
addFormSubmitListeners() {
|
||||
mQuery(this.getForm()).on('submit:success', (e, requestUrl, response) => {
|
||||
const lastRequestUrlPart = requestUrl.split('/').pop();
|
||||
const lastResponseUrlPart = response.route.split('/').pop();
|
||||
|
||||
// Check if the form was submitted for a new entity and the response contains the entity id
|
||||
// The success response code alone does not guarantee that the form was saved,
|
||||
// so we need to validate the URL changes and the presence of the entity id
|
||||
if (lastRequestUrlPart === 'new' && !isNaN(lastResponseUrlPart)){
|
||||
// Remove the local storage item for the newly created entity after successful form submission
|
||||
this.removeStorageItemById(`gjs-${this.mode}-${Mautic.builderTheme}-new`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
// update the storage content only when the restore prompt is not available
|
||||
if (!this.restoreMessage) {
|
||||
const editorContent = this.getEditorContent();
|
||||
const dateTime = new Date().toISOString();
|
||||
const stackItemId = this.getStackItemId();
|
||||
const contentWithDateTime = { id: stackItemId, content: editorContent, date: dateTime };
|
||||
this.saveStorageItem(contentWithDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
const stackItemId = this.getStackItemId();
|
||||
const storageItem = this.getStorageItemById(stackItemId);
|
||||
if (storageItem) {
|
||||
this.editor.setComponents(storageItem.content);
|
||||
}
|
||||
}
|
||||
|
||||
getEditorContent() {
|
||||
let content;
|
||||
if (ContentService.isMjmlMode(this.editor)) {
|
||||
content = MjmlService.getEditorMjmlContent(this.editor);
|
||||
} else {
|
||||
content = ContentService.getEditorHtmlContent(this.editor);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
getStackItemId() {
|
||||
const entityId = this.getFormEntityId(this.mode === 'page' ? 'page' : 'emailform')
|
||||
return `gjs-${this.mode}-${Mautic.builderTheme}-${entityId}`;
|
||||
}
|
||||
|
||||
saveStorageItem(item) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
const index = stack.findIndex(existingItem => existingItem.id === item.id);
|
||||
if (index !== -1) {
|
||||
// If the item already exists, update it
|
||||
stack[index] = item;
|
||||
} else {
|
||||
// If the item doesn't exist, push it to the stack
|
||||
if (stack.length >= this.maxStorageItems) {
|
||||
// Ensure that the stack does not exceed the maximum allowed number of items
|
||||
// to prevent web storage from exceeding its 10MiB per domain limit
|
||||
stack.pop(); // Remove the oldest item
|
||||
}
|
||||
stack.push(item);
|
||||
}
|
||||
|
||||
stack.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(stack));
|
||||
}
|
||||
|
||||
getStorageItemById(id) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
return stack.find(item => item.id === id);
|
||||
}
|
||||
|
||||
removeStorageItemById(id) {
|
||||
const stack = JSON.parse(localStorage.getItem(this.storageKey)) || [];
|
||||
const index = stack.findIndex(item => item.id === id);
|
||||
if (index !== -1) {
|
||||
stack.splice(index, 1);
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(stack));
|
||||
}
|
||||
}
|
||||
|
||||
formatDateTime(dateTime) {
|
||||
return new Date(dateTime).toISOString().slice(0, 16).replace('T', ' ');
|
||||
}
|
||||
|
||||
getFormEntityId(name) {
|
||||
const form = document.querySelector(`form[name="${name}"]`);
|
||||
const actionUrl = form.getAttribute('action');
|
||||
const urlParts = actionUrl.split('/');
|
||||
const lastPart = urlParts.pop();
|
||||
if (isNaN(lastPart)) {
|
||||
return 'new';
|
||||
} else {
|
||||
return lastPart;
|
||||
}
|
||||
}
|
||||
|
||||
getForm() {
|
||||
return document.querySelector(this.mode === 'page' ? 'form[name="page"]' : 'form[name="emailform"]')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user