diff --git a/dist/blocks.js b/dist/blocks.js index 450ce13..258f4ec 100644 --- a/dist/blocks.js +++ b/dist/blocks.js @@ -6,12 +6,13 @@ export default ((editor, opts = {}) => { const bm = editor.BlockManager; const blocks = bm.getAll(); const mode = ContentService.getMode(editor); + if (mode === ContentService.modeEmailMjml) { const blockMjml = new BlocksMjml(editor); blockMjml.addBlocks(); - } + } // a add button block for landing page + - // a add button block for landing page if (mode === ContentService.modePageHtml) { const buttonBlock = new ButtonBlock(editor); buttonBlock.addButtonBlock(); @@ -19,96 +20,104 @@ export default ((editor, opts = {}) => { // Add Dynamic Content block only for email modes const dcb = new DynamicContentBlocks(editor, opts); dcb.addDynamciContentBlock(); - } + } // Add icon to mj-hero + - // Add icon to mj-hero if (typeof bm.get('mj-hero') !== 'undefined') { bm.get('mj-hero').set({ attributes: { class: 'gjs-fonts gjs-f-hero' } }); - } + } // Delete mj-wrapper + - // Delete mj-wrapper if (typeof bm.get('mj-wrapper') !== 'undefined') { bm.remove('mj-wrapper'); - } + } // All block inside Blocks category + - // All block inside Blocks category blocks.forEach(block => { block.set({ category: Mautic.translate('grapesjsbuilder.categoryBlockLabel') }); }); - /* * Custom block inside Sections category */ - // MJML columns + if (typeof bm.get('mj-1-column') !== 'undefined') { bm.get('mj-1-column').set({ label: Mautic.translate('grapesjsbuilder.components.names.oneColumn'), category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('mj-2-columns') !== 'undefined') { bm.get('mj-2-columns').set({ label: Mautic.translate('grapesjsbuilder.components.names.twoColumn'), category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('mj-3-columns') !== 'undefined') { bm.get('mj-3-columns').set({ label: Mautic.translate('grapesjsbuilder.components.names.threeColumn'), category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('mj-37-columns') !== 'undefined') { bm.get('mj-37-columns').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); - } + } // Newsletter columns + - // Newsletter columns if (typeof bm.get('sect100') !== 'undefined') { bm.get('sect100').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('sect50') !== 'undefined') { bm.get('sect50').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('sect30') !== 'undefined') { bm.get('sect30').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('sect37') !== 'undefined') { bm.get('sect37').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); - } + } // Webpage columns + - // Webpage columns if (typeof bm.get('column1') !== 'undefined') { bm.get('column1').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('column2') !== 'undefined') { bm.get('column2').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('column3') !== 'undefined') { bm.get('column3').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') }); } + if (typeof bm.get('column3-7') !== 'undefined') { bm.get('column3-7').set({ category: Mautic.translate('grapesjsbuilder.categorySectionLabel') diff --git a/dist/blocks/blocks.mjml.js b/dist/blocks/blocks.mjml.js index 59a1b99..ddec8d7 100644 --- a/dist/blocks/blocks.mjml.js +++ b/dist/blocks/blocks.mjml.js @@ -1,13 +1,15 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + export default class BlocksMjml { constructor(editor) { _defineProperty(this, "blockManager", void 0); + _defineProperty(this, "editor", void 0); + this.editor = editor; this.blockManager = editor.BlockManager; } + addBlocks() { const sections37 = `Content 1 Content 2`; @@ -85,4 +87,5 @@ export default class BlocksMjml { content: `${listItem}` }); } + } \ No newline at end of file diff --git a/dist/buttonBlock.js b/dist/buttonBlock.js index 038850b..8bf9266 100644 --- a/dist/buttonBlock.js +++ b/dist/buttonBlock.js @@ -1,11 +1,12 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + export default class ButtonBlock { constructor(editor) { _defineProperty(this, "blockManager", void 0); + this.blockManager = editor.BlockManager; } + addButtonBlock() { const style = ``; + })}`; // get a DocumentHTML from the string - // get a DocumentHTML from the string - const htmlDocument = parser.parseFromString(htmlCombined, 'text/html'); + const htmlDocument = parser.parseFromString(htmlCombined, 'text/html'); // if no header is set on the canvas, replace it with existing from theme - // if no header is set on the canvas, replace it with existing from theme if (!htmlDocument.head.innerHTML && originalContent.head.innerHTML) { logger.debug('Set head based on the original content', { head: originalContent.head.innerHTML }); htmlDocument.head.innerHTML = originalContent.head.innerHTML; } + return htmlDocument; } - /** * Get complete current html. Including doctype and original header. * @returns string */ + + static getEditorHtmlContent(editor) { if (!editor) { throw new Error('Editor is required.'); } + const contentDocument = ContentService.getCanvasAsHtmlDocument(editor); + if (!contentDocument || !contentDocument.body) { throw new Error('No html content found'); } + return ContentService.serializeHtmlDocument(contentDocument); } - /** * Serialize a DocumentHTML Object to a string * @param {DocumentHTML} contentDocument */ + + static serializeHtmlDocument(contentDocument) { if (!contentDocument || !(contentDocument instanceof HTMLDocument)) { throw new Error('No Html Document'); - } + } // @todo add the lang parameter. E.g. + - // @todo add the lang parameter. E.g. return `${ContentService.serializeDoctype(contentDocument.doctype)}${contentDocument.head.outerHTML}${contentDocument.body.outerHTML}`; } - /** * Returns the correct string for valid (HTML5) doctypes, eg: * @@ -89,40 +95,50 @@ export default class ContentService { * @param {DocumentType} * @returns string */ + + static serializeDoctype(doctype) { if (!doctype) { return ''; } + return new XMLSerializer().serializeToString(doctype); } - /** * Get the selected themes original or the users last saved * content from the db. Loaded via Mautic PHP into the textarea. * @returns HTMLDocument */ + + static getOriginalContentHtml() { // Parse HTML theme/template const parser = new DOMParser(); const textareaHtml = mQuery('textarea.builder-html'); const doc = parser.parseFromString(textareaHtml.val(), 'text/html'); + if (!doc.body.innerHTML || !doc.head.innerHTML) { throw new Error('No valid HTML template found'); } + return doc; } - /** * Extract all stylesheets from the template * @todo use DocumentHTML Styles directly */ + + static getStyles() { const content = ContentService.getOriginalContentHtml(); + if (!content.head) { return []; } + const links = content.head.querySelectorAll('link'); const styles = []; + if (links) { links.forEach(link => { if (link && link.rel === 'stylesheet') { @@ -130,9 +146,14 @@ export default class ContentService { } }); } + return styles; } + } + _defineProperty(ContentService, "modeEmailHtml", 'email-html'); + _defineProperty(ContentService, "modeEmailMjml", 'email-mjml'); + _defineProperty(ContentService, "modePageHtml", 'page-html'); \ No newline at end of file diff --git a/dist/dynamicContent/dynamicContent.blocks.js b/dist/dynamicContent/dynamicContent.blocks.js index 78743af..d4a942b 100644 --- a/dist/dynamicContent/dynamicContent.blocks.js +++ b/dist/dynamicContent/dynamicContent.blocks.js @@ -1,15 +1,18 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + export default class DynamicContentBlocks { constructor(editor, opts = {}) { _defineProperty(this, "editor", void 0); + _defineProperty(this, "opts", void 0); + _defineProperty(this, "blockManager", void 0); + this.editor = editor; this.opts = opts; this.blockManager = this.editor.BlockManager; } + addDynamciContentBlock() { this.blockManager.add('dynamic-content', { label: Mautic.translate('grapesjsbuilder.dynamicContentBlockLabel'), @@ -28,4 +31,5 @@ export default class DynamicContentBlocks { } }); } + } \ No newline at end of file diff --git a/dist/dynamicContent/dynamicContent.commands.js b/dist/dynamicContent/dynamicContent.commands.js index 18cce84..56de756 100644 --- a/dist/dynamicContent/dynamicContent.commands.js +++ b/dist/dynamicContent/dynamicContent.commands.js @@ -1,20 +1,23 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + import Logger from '../logger'; import MjmlService from '../mjml/mjml.service'; import DynamicContentService from './dynamicContent.service'; export default class DynamicContentCommands { constructor(editor) { _defineProperty(this, "editor", void 0); + _defineProperty(this, "dcPopup", void 0); + _defineProperty(this, "dcService", void 0); + _defineProperty(this, "logger", void 0); + this.dcService = new DynamicContentService(editor); this.logger = new Logger(editor); - } + } // eslint-disable-next-line class-methods-use-this + - // eslint-disable-next-line class-methods-use-this stopDynamicContentPopup() { // Destroy Dynamic Content editors and write the contents to the textarea var logger = this.logger; @@ -30,14 +33,16 @@ export default class DynamicContentCommands { } }); } + this.dcService.updateDcStoreItem(); } - /** * Update and wire all dynamic content components on the canvas to * human readable texts/slots/components. * E.g. if they are initialized from a {token} */ + + updateComponentsFromDcStore() { const components = this.dcService.getDcComponents(); components.forEach(comp => { @@ -48,7 +53,6 @@ export default class DynamicContentCommands { } }); } - /** * Convert dynamic content components to tokens * Dynamic Content => {dynamicContent} @@ -57,6 +61,8 @@ export default class DynamicContentCommands { * @returns integer nr of dynamic content slots found */ // eslint-disable-next-line class-methods-use-this + + convertDynamicContentComponentsToTokens(editor) { // get all dynamic content elements loaded in the editor const dynamicContents = editor.DomComponents.getWrapper().find('[data-slot="dynamicContent"]'); @@ -66,19 +72,20 @@ export default class DynamicContentCommands { dynamicContents.forEach(dynamicContent => { const attributes = dynamicContent.getAttributes(); const decId = attributes['data-param-dec-id']; + if (!decId) { this.logger.debug('DC: Expected a Dynamic Content component', { dynamicContent }); throw new Error('No Dynamic Content component'); } + const dynConId = DynamicContentCommands.getDcStoreId(attributes['data-param-dec-id']); const dynConTarget = mQuery(dynConId); const dynConName = dynConTarget.find(`${dynConId}_tokenName`).val(); - const dynConToken = `{dynamiccontent="${dynConName}"}`; - - // Clear id because it's reloaded by Mautic and this prevent slot to be destroyed by GrapesJs destroy event on close. + const dynConToken = `{dynamiccontent="${dynConName}"}`; // Clear id because it's reloaded by Mautic and this prevent slot to be destroyed by GrapesJs destroy event on close. // dynamicContent.addAttributes({ 'data-param-dec-id': '' }); + this.logger.debug("DC: Replaced component's content with its token", { dynamicContent, dynConToken @@ -88,7 +95,6 @@ export default class DynamicContentCommands { }); return dynamicContents.length; } - /** * Build and display the Dynamic Content editor popup/modal window. * Hint: the passed in editor is the main grapesjs editor. @@ -96,6 +102,8 @@ export default class DynamicContentCommands { * @param {Model} component The current grapesjs component */ // eslint-disable-next-line class-methods-use-this + + showDynamicContentPopup(editor, sender, options) { this.dcPopup = DynamicContentCommands.buildDynamicContentPopup(); this.addDynamicContentEditor(editor, options); @@ -103,111 +111,116 @@ export default class DynamicContentCommands { const modal = editor.Modal; modal.setTitle(title); editor.Modal.setContent(this.dcPopup); - modal.open(); + modal.open(); // Set up dynamic content editors if present - // Set up dynamic content editors if present - Mautic.setDynamicContentEditors(Mautic.getBuilderContainer()); + Mautic.setDynamicContentEditors(Mautic.getBuilderContainer()); // When a new Dynamic Content filter (tab) is added, we want to turn the editor into CKEditor. - // When a new Dynamic Content filter (tab) is added, we want to turn the editor into CKEditor. Mautic.dynamicContentAddNewFilterListener(textarea => { Mautic.ConvertFieldToCkeditor(textarea, {}); - }); + }); // When a new Dynamic Content item (slot) is added, we want to turn the editor into CKEditor. - // When a new Dynamic Content item (slot) is added, we want to turn the editor into CKEditor. Mautic.dynamicContentAddNewItemListener(textarea => { Mautic.ConvertFieldToCkeditor(textarea, {}); }); modal.onceClose(() => editor.stopCommand('preset-mautic:dynamic-content-open')); } - /** * Build the basic popup/modal frame to hold the dynamic content editor * @returns HTMLDivElement */ + + static buildDynamicContentPopup() { - const codePopup = document.createElement('div'); - // codePopup.setAttribute('id', 'codePopup'); + const codePopup = document.createElement('div'); // codePopup.setAttribute('id', 'codePopup'); + codePopup.setAttribute('id', 'dynamic-content-popup'); return codePopup; } - /** * Load Dynamic Content editor and append to the codePopup Modal */ + + addDynamicContentEditor(editor, options) { const { target } = options; const dcComponent = target || editor.getSelected(); + if (!dcComponent) { throw new Error('No DC Components found'); } - const attributes = dcComponent.getAttributes(); - // const popupContent = dcPopup.querySelector('#dynamic-content-popup'); + const attributes = dcComponent.getAttributes(); // const popupContent = dcPopup.querySelector('#dynamic-content-popup'); // do we need both? // console.warn({ dcPopup }); // console.warn({ popupContent }); - // get the dynamic content editor + const dcEditor = mQuery(DynamicContentCommands.getDcStoreId(attributes['data-param-dec-id'])); + if (dcEditor.length <= 0) { throw new Error(`No Dynamic Content editor found for decId: '${attributes['data-param-dec-id']}'`); - } + } // Show if hidden - // Show if hidden - dcEditor.removeClass('fade'); - // Hide delete default button - dcEditor.find('.tab-pane:first').find('.remove-item').hide(); - // Clean existing editor - mQuery(this.dcPopup).empty(); - // Insert inside popup + dcEditor.removeClass('fade'); // Hide delete default button + + dcEditor.find('.tab-pane:first').find('.remove-item').hide(); // Clean existing editor + + mQuery(this.dcPopup).empty(); // Insert inside popup + mQuery(this.dcPopup).append(dcEditor.detach()); } + linkComponentToStoreItem(edtr, sender, options) { // Add a new DC HTML store item, if it doesnt exist. // Hint: the first dynamic content item (tab) is created from php: #emailform_dynamicContent_0 this.dcService.linkComponentToStoreItem(options.component); } - /** * Delete DynamicContent on Mautic side * * @param component */ // eslint-disable-next-line class-methods-use-this + + deleteDynamicContentStoreItem(editor, sender, options) { const { component } = options; const attributes = component.getAttributes(); + if (!attributes['data-param-dec-id']) { this.logger.warning('no dec-id found. Can not delete', attributes); } + const dcStoreId = DynamicContentCommands.getDcStoreId(attributes['data-param-dec-id']); const dcStoreItem = mQuery(dcStoreId); + if (!dcStoreItem) { this.logger.warning('No DynamicContent store item found', { dcStoreId }); - } + } // remove the store item + + + dcStoreItem.find('a.remove-item:first').click(); // remove store navigation item - // remove the store item - dcStoreItem.find('a.remove-item:first').click(); - // remove store navigation item const dynCon = mQuery('.dynamicContentFilterContainer').find(`a[href='${dcStoreId}']`); + if (!dynCon || !dynCon.parent()) { this.logger.warning('No DynamicContent store item to delete found', { dcStoreId }); } + dynCon.parent().remove(); this.logger.debug('DC: DynamicContent store item removed', { dcStoreId }); } - /** * Get the DynamicContent identifier of the html store item * based on the token nr (decId) @@ -215,11 +228,16 @@ export default class DynamicContentCommands { * @param {integer} decId * @returns string */ + + static getDcStoreId(decId) { const id = decId - 1; + if (id < 0) { throw new Error('no dynamic content ID'); } + return `#emailform_dynamicContent_${id}`; } + } \ No newline at end of file diff --git a/dist/dynamicContent/dynamicContent.domcomponents.js b/dist/dynamicContent/dynamicContent.domcomponents.js index e5e2cdb..13d3775 100644 --- a/dist/dynamicContent/dynamicContent.domcomponents.js +++ b/dist/dynamicContent/dynamicContent.domcomponents.js @@ -1,21 +1,20 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + import ContentService from '../content.service'; import DynamicContentService from './dynamicContent.service'; export default class DynamicContentDomComponents { constructor() { _defineProperty(this, "dcService", void 0); } + static addDynamicContentType(editor) { const dc = editor.DomComponents; const baseTypeName = ContentService.isMjmlMode(editor) ? 'mj-text' : 'text'; const tagName = ContentService.isMjmlMode(editor) ? 'mj-text' : 'div'; const baseType = dc.getType(baseTypeName); const baseModel = baseType.model; - const model = baseModel.extend({ + const model = { defaults: { - ...baseModel.prototype.defaults, name: 'Dynamic Content', tagName, draggable: '[data-gjs-type=cell],[data-gjs-type=mj-column]', @@ -27,6 +26,7 @@ export default class DynamicContentDomComponents { 'data-gjs-type': 'dynamic-content', // Type for GrapesJS 'data-slot': 'dynamicContent' // used to find the DC component on the canvas for e.g. token transformation + } }, @@ -37,11 +37,11 @@ export default class DynamicContentDomComponents { // link component to the corresponding html store item this.em.get('Commands').run('preset-mautic:link-component-to-store-item', { component: this - }); + }); // Add toolbar edit button if it's not already in - // Add toolbar edit button if it's not already in const toolbar = this.get('toolbar'); const id = 'toolbar-dynamic-content'; + if (!toolbar.filter(tlb => tlb.id === id).length) { toolbar.unshift({ id, @@ -51,7 +51,8 @@ export default class DynamicContentDomComponents { } }); } - } + }, + // @todo: show the store items default content on the canvas // updated(property, value, prevValue) { // console.log('Local hook: model.updated', { @@ -60,7 +61,6 @@ export default class DynamicContentDomComponents { // prevValue, // }); // }, - }, { // Dynamic Content component detection isComponent(el) { if (el.getAttribute && el.getAttribute('data-slot') === 'dynamicContent') { @@ -68,16 +68,19 @@ export default class DynamicContentDomComponents { type: 'dynamic-content' }; } + return false; } - }); - const view = baseType.view.extend({ + + }; + const view = { attributes: { style: 'pointer-events: all; display: table; width: 100%;user-select: none;' }, events: { dblclick: 'onActive' }, + // replace token with human readable view onRender(el) { const dcService = new DynamicContentService(editor); @@ -86,15 +89,15 @@ export default class DynamicContentDomComponents { this.el.innerHTML = dcItem.content; dcService.logger.debug('DC: Updated view', dcItem); }, + // open the dynamic content modal if the editor is added or double clicked onActive() { - const target = this.model; - // open the editor in the popup + const target = this.model; // open the editor in the popup + this.em.get('Commands').run('preset-mautic:dynamic-content-open', { target }); - } - // does not work: gets removed when Sorting (by grapesjs) + } // does not work: gets removed when Sorting (by grapesjs) // removed() { // // Delete dynamic-content on Mautic side // const component = this.model; @@ -102,12 +105,14 @@ export default class DynamicContentDomComponents { // .get('Commands') // .run('preset-mautic:dynamic-content-delete-store-item', { component }); // }, - }); - // add the Dynamic Content component + + }; // add the Dynamic Content component + dc.addType('dynamic-content', { model, view }); } + } \ No newline at end of file diff --git a/dist/dynamicContent/dynamicContent.events.js b/dist/dynamicContent/dynamicContent.events.js index 861588c..e0347c6 100644 --- a/dist/dynamicContent/dynamicContent.events.js +++ b/dist/dynamicContent/dynamicContent.events.js @@ -1,19 +1,20 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + import DynamicContentCommands from './dynamicContent.commands'; import DynamicContentService from './dynamicContent.service'; export default class DynamicContentEvents { constructor(editor) { _defineProperty(this, "editor", void 0); + _defineProperty(this, "dcService", void 0); + this.editor = editor; this.dcService = new DynamicContentService(this.editor); this.dccmd = new DynamicContentCommands(this.editor); - } - - // @todo merge events and listeners. or move this to the component itself as a + } // @todo merge events and listeners. or move this to the component itself as a // local listener. see create-new-dynamic-content-store-item + + onComponentRemove() { this.editor.on('component:remove', component => { // Delete dynamic-content on Mautic side @@ -24,4 +25,5 @@ export default class DynamicContentEvents { } }); } + } \ No newline at end of file diff --git a/dist/dynamicContent/dynamicContent.service.js b/dist/dynamicContent/dynamicContent.service.js index 570d91e..1aa490b 100644 --- a/dist/dynamicContent/dynamicContent.service.js +++ b/dist/dynamicContent/dynamicContent.service.js @@ -1,6 +1,5 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + import Logger from '../logger'; export default class DynamicContentService { /** @@ -12,15 +11,16 @@ export default class DynamicContentService { /** * Components currently on the canvas/editor */ - constructor(editor) { _defineProperty(this, "dcStoreItems", []); + _defineProperty(this, "dcComponents", []); + _defineProperty(this, "editor", void 0); + this.logger = new Logger(editor); this.editor = editor; } - /** * Get the name of the dynamic content element. * Used as identifier @@ -28,74 +28,85 @@ export default class DynamicContentService { * @param {GrapesJS Component} component * @returns string | null */ + + getTokenName(component) { const regex = RegExp(/\{dynamiccontent="(.*)"\}/, 'g'); const content = component.get('content'); const regexEx = regex.exec(content); + if (!regexEx || !regexEx[1]) { this.logger.debug('DC: No dynamic content tokens to get', { content }); return null; } + return regexEx[1]; } - /** * Get dec ID from the store items identifier: e.g. #emailform_dynamicContent_1 * @returns integer */ + + static getDecId(storeItemIdentifier) { // dec id starts with 1, so we add +1 const decId = parseInt(storeItemIdentifier.replace(/[^0-9]/g, ''), 10) + 1; + if (decId <= 0) { throw new Error('DC: no valid decId'); } + return decId; } - /** * Returns the decId from the component * @returns integer */ + + static getDataParamDecid(component) { return parseInt(component.getAttributes()['data-param-dec-id'], 10) || 0; } - /** * Link the component on the canvas with the item in the HTML store * If it does not exist, create a new store item */ + + linkComponentToStoreItem(component) { // get components data-param-dec-id (can come from token from db) let decId = DynamicContentService.getDataParamDecid(component); + if (decId > 0) { this.logger.debug('DC: Already wired up', { decId }); return decId; - } - // not wired up yet - get component token id + } // not wired up yet - get component token id + + const tokenName = this.getTokenName(component); const tokenNr = DynamicContentService.getDecId(tokenName); decId = tokenNr; + if (decId > 0) { this.logger.debug('DC: Using decId from token nr', { tokenName, decId }); return decId; - } + } // double check if a store item exists - if not found - create new store item + - // double check if a store item exists - if not found - create new store item if (!this.getStoreItem(decId)) { this.createNewStoreItem(component); - } + } // return components dec-id (also from the new one) + - // return components dec-id (also from the new one) return DynamicContentService.getDataParamDecid(component); } - /** * Get the content from the Html store and put the default content on the canvas. * Creates a store item (filter) in Mautic Form if new. @@ -104,32 +115,34 @@ export default class DynamicContentService { * * @param {GrapesJS Component} component */ + + updateComponentFromDcStore(component) { // get the item/tab matching the dynamic content on the canvas let dataParamDecId = DynamicContentService.getDataParamDecid(component); - let dcItem = this.getStoreItem(dataParamDecId); + let dcItem = this.getStoreItem(dataParamDecId); // Load the html store item - // Load the html store item dataParamDecId = DynamicContentService.getDataParamDecid(component); - dcItem = this.getStoreItem(dataParamDecId); + dcItem = this.getStoreItem(dataParamDecId); // Update the Grapesjs component with the content from the HTML store item - // Update the Grapesjs component with the content from the HTML store item this.updateComponent(component, dcItem); return true; } - /** * if the editors modal is closed/stopped the Components content visible * on the canvas and the html store item has to be updated */ + + updateDcStoreItem() { // For the editing inside grapesjs the dynamcicontent popup is moved to the "grapesjs dom" // so it has to be moved back to the Mautic email form for saving. - const modalContent = mQuery('#dynamic-content-popup'); - // On modal close -> move editor within Mautic + const modalContent = mQuery('#dynamic-content-popup'); // On modal close -> move editor within Mautic + if (!modalContent) { throw new Error('DC: No dynamic content popup found'); } + const dynamicContentContainer = mQuery('#dynamicContentContainer'); const content = mQuery(modalContent).contents().first(); dynamicContentContainer.append(content); @@ -139,37 +152,38 @@ export default class DynamicContentService { id: content.attr('id') }); } - /** * Get a dynamic content item from its html store */ + + getStoreItem(decId) { // get all items this.getDcStoreItems(); const item = this.dcStoreItems.find(itm => itm.decId === decId); return item; } - /** * Set the html/content of the visible component on the canvas */ + + updateComponent(component, dcItem) { if (!component || !dcItem) { throw new Error('No component or dynamic content item'); } + this.logger.debug('DC: Updating DC component with values from store', { dcItem - }); - // Update the component on the canvas with new values from the html store + }); // Update the component on the canvas with new values from the html store // and link it with the id to the html store // needed for new components + component.addAttributes({ 'data-param-dec-id': dcItem.decId }); component.set('content', dcItem.content); - return component; - - // gets the default content to be displayed on the canvas. Should have been replaced by the `dcItem.content` + return component; // gets the default content to be displayed on the canvas. Should have been replaced by the `dcItem.content` // let dynConContent = ''; // if (dcItem.decId) { // const dynConContainer = mQuery(dcTarget.htmlId).find(dcTarget.content); @@ -181,7 +195,6 @@ export default class DynamicContentService { // } // } } - /** * If dynamic content item in html store doesn't exist -> create * @todo replace mQuery('#dynamicContentTabs') with class property @@ -189,14 +202,15 @@ export default class DynamicContentService { * @param {GrapesJS Component} component * @param {string} */ + + createNewStoreItem(component) { const storeItemIdentifier = Mautic.createNewDynamicContentItem(mQuery); const decId = DynamicContentService.getDecId(storeItemIdentifier); component.addAttributes({ 'data-param-dec-id': decId - }); + }); // Replace token on canvas with user facing name: dcName - // Replace token on canvas with user facing name: dcName component.set('content', `Dynamic Content ${decId}`); this.logger.debug('DC: Created a new Dynamic Content item in store', { decId, @@ -204,7 +218,6 @@ export default class DynamicContentService { }); return component; } - /** * Extract the dynamic content index and the html id from a string: * e.g. href from dcTarget @@ -213,22 +226,25 @@ export default class DynamicContentService { * * @param {string} identifier e.g. http://localhost:1234/#emailform_dynamicContent_1 */ + + static getDcTarget(identifier) { const regex = RegExp(/(#emailform_dynamicContent_)(\d*)/, 'g'); const result = regex.exec(identifier); + if (!result || result.length !== 3) { throw new Error(`No DynamicContent target found: ${identifier}`); } + return { htmlId: `${result[1]}${result[2]}`, // #emailform_dynamicContent_1 decId: parseInt(result[2], 10) + 1, // 2 content: `${result[1]}${result[2]}_content` // #emailform_dynamicContent_1_content - }; - } - // /** + }; + } // /** // * Extract the dynamic content id from an id string: // * // * @param {string} identifier e.g. emailform_dynamicContent_1 @@ -249,27 +265,30 @@ export default class DynamicContentService { * @param {string} id id of the input field * @returns string of the field */ + + static getDcName(target) { return `Dynamic Content ${target.decId}`; } - /** * Get the default content * @param {string} id id of the textarea * @returns string with the html in the textarea */ + + static getDcContent(target) { return mQuery(target.id).val() || DynamicContentService.getDcName(target); } - /** * Get all Dynamic Content items from the HTML store * @returns array of objects with title and href */ + + getDcStoreItems() { - this.dcStoreItems = []; + this.dcStoreItems = []; // #dynamicContentContainer - // #dynamicContentContainer mQuery('.dynamic-content').each((index, value) => { const dcTarget = DynamicContentService.getDcTarget(`#${value.id}`); this.dcStoreItems.push({ @@ -280,24 +299,26 @@ export default class DynamicContentService { name: DynamicContentService.getDcName(dcTarget), // Dynamic Content 1 content: DynamicContentService.getDcContent(dcTarget) // Default Dynamic Content + }); - }); + }); // one is always set from php - // one is always set from php if (!this.dcStoreItems[0]) { throw Error('No dynamic content store item found!'); } } - /** * Get all the dynamic content components currently * on the canvas (in the editor). * @return {GrapesJS Component} array */ + + getDcComponents() { if (!this.editor) { throw new Error('no Editor'); } + this.dcComponents = []; const wrapper = this.editor.getWrapper(); const dcCompoments = wrapper.find('[data-gjs-type="dynamic-content"]'); @@ -305,8 +326,10 @@ export default class DynamicContentService { if (!comp.is('dynamic-content')) { throw new Error('no dynamic-content component'); } + this.dcComponents.push(comp); }); return this.dcComponents; } + } \ No newline at end of file diff --git a/dist/editorFonts/editorFonts.service.js b/dist/editorFonts/editorFonts.service.js index 81fb86f..6143956 100644 --- a/dist/editorFonts/editorFonts.service.js +++ b/dist/editorFonts/editorFonts.service.js @@ -2,23 +2,28 @@ import ContentService from '../content.service'; export default class EditorFontsService { static loadEditorFonts(editor) { const styleManager = editor.StyleManager; + if (!styleManager) { // eslint-disable-next-line no-console console.error('No GrapesJS Style Manager found.'); return; } + const fontProperty = styleManager.getProperty('typography', 'font-family'); + if (!fontProperty) { // eslint-disable-next-line no-console console.error('No font properties found in the typography sector.'); return; } + const fontList = fontProperty.get('list'); EditorFontsService.updateFontList(editor, fontList); EditorFontsService.sortFontList(fontList); fontProperty.set('list', fontList); styleManager.render(); } + static updateFontList(editor, fontList) { const canvasHead = editor.Canvas.getDocument().head; mauticEditorFonts.forEach(item => { @@ -27,6 +32,7 @@ export default class EditorFontsService { value: item.font, name: item.name }); + if (item.url && canvasHead) { EditorFontsService.appendFontStyleLink(canvasHead, item.url); } @@ -34,10 +40,12 @@ export default class EditorFontsService { }); return fontList; } + static sortFontList(fontList) { fontList.sort((a, b) => a.name < b.name ? -1 : 1); return fontList; } + static addFontLinksToHtml(htmlCode) { const htmlDoc = new DOMParser().parseFromString(htmlCode, 'text/html'); const headElem = htmlDoc.head; @@ -53,10 +61,12 @@ export default class EditorFontsService { }); return ContentService.serializeHtmlDocument(htmlDoc); } + static appendFontStyleLink(head, url) { const linkElem = EditorFontsService.createStylesheetLink(url); return head.appendChild(linkElem); } + static createStylesheetLink(url) { const linkElem = document.createElement('link'); linkElem.rel = 'stylesheet'; @@ -64,4 +74,5 @@ export default class EditorFontsService { linkElem.href = url; return linkElem; } + } \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index ab32c5b..8c043b4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -14,9 +14,8 @@ export default ((editor, opts = {}) => { ...opts }; const logger = new Logger(editor); - logger.addListener(config.logFilter, editor); + logger.addListener(config.logFilter, editor); // Extend the original `image` and add a confirm dialog before removing it - // Extend the original `image` and add a confirm dialog before removing it am.addType('image', { // As you adding on top of an already defined type you can avoid indicating // `am.getType('image').view.extend({...` the editor will do it by default @@ -28,17 +27,16 @@ export default ((editor, opts = {}) => { e.stopImmediatePropagation(); const { model - } = this; + } = this; // eslint-disable-next-line no-alert, no-restricted-globals - // eslint-disable-next-line no-alert, no-restricted-globals if (confirm(Mautic.translate('grapesjsbuilder.deleteAssetConfirmText'))) { model.collection.remove(model); } } + } - }); + }); // Load other parts - // Load other parts loadCommands(editor, config); loadComponents(editor, config); loadEvents(editor, config); diff --git a/dist/listeners.js b/dist/listeners.js index b65dac6..0dce53e 100644 --- a/dist/listeners.js +++ b/dist/listeners.js @@ -1,5 +1,4 @@ -export default (() => { - // @todo is this needed? why do we copy the original content? +export default (() => {// @todo is this needed? why do we copy the original content? // this.editor.on('run:mautic-editor-email-mjml-close:before', () => { // mQuery('textarea.builder-html').val(this.getBody()); // }); diff --git a/dist/logger.js b/dist/logger.js index 609c22c..89f2f6c 100644 --- a/dist/logger.js +++ b/dist/logger.js @@ -1,36 +1,41 @@ -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + export default class Logger { constructor(editor) { _defineProperty(this, "editor", void 0); + if (!editor) { throw new Error('Editor is required'); } + this.editor = editor; } + debug(msg, params = {}) { this.log(msg, params, 'debug'); } + info(msg, params = {}) { this.log(msg, params, 'info'); } + warning(msg, params = {}) { this.log(msg, params, 'warning'); } + error(msg, params = {}) { this.log(msg, params, 'error'); } - /** * * @param {string} msg log message * @param {object} params optional params * @param {string} level log level */ + + log(msg, params, level = 'debug') { - const options = { - ...{ + const options = { ...{ ns: Logger.namespace, level }, @@ -38,18 +43,17 @@ export default class Logger { }; this.editor.log(msg, options); } - /** * What kind of logs to display * @param {string} filter `log`, `log:info`, `grapesjs-preset`, `grapesjs-preset:info` */ + + addListener(filter = 'log:debug') { // find the severity for debug, info, warning. - const displaySeverity = Logger.filters.findIndex(element => element === filter); + const displaySeverity = Logger.filters.findIndex(element => element === filter); // severity only works with items in Logger.filters. All other filters are applied directly - // severity only works with items in Logger.filters. All other filters are applied directly - if (displaySeverity === -1) { - // this.editor.on(filter, (msg, opts) => console.info(msg, opts)); + if (displaySeverity === -1) {// this.editor.on(filter, (msg, opts) => console.info(msg, opts)); } else { // listen for all logs with a severity > than the current setting. Logger.filters.forEach((item, severity) => { @@ -60,6 +64,9 @@ export default class Logger { }); } } + } + _defineProperty(Logger, "namespace", 'grapesjs-preset'); + _defineProperty(Logger, "filters", ['log:debug', 'log:info', 'log:warning']); \ No newline at end of file diff --git a/dist/mjml/mjml.service.js b/dist/mjml/mjml.service.js index 1f37fae..45378e7 100644 --- a/dist/mjml/mjml.service.js +++ b/dist/mjml/mjml.service.js @@ -8,12 +8,13 @@ export default class MjmlService { static getOriginalContentMjml() { return mQuery('textarea.builder-mjml').val(); } - /** * Get the editors mjml and transform it to html * @param {Grapesjs Editor} editor * @returns string */ + + static getEditorHtmlContent(editor) { // Try catch for mjml parser error try { @@ -26,32 +27,37 @@ export default class MjmlService { console.warn(error.message); alert('Errors inside your template. Template will not be saved.'); } + return ''; } - /** * Get the editors mjml * @param {Grapesjs Editor} editor * @returns string */ + + static getEditorMjmlContent(editor) { // cleanId: Remove unnecessary IDs (eg. those created automatically) return editor.getHtml({ cleanId: true }).trim(); } - /** * Transform MJML to HTML * @todo show validation errors in the UI * @returns string */ + + static mjmlToHtml(mjml, endpoint = '') { let html = ''; + try { if (typeof mjml !== 'string' || !mjml.includes('')) { throw new Error('No valid MJML provided'); } + if (endpoint !== '') { html = MjmlService.mjmlToHtmlViaEndpoint(mjml, endpoint); } else { @@ -65,13 +71,15 @@ export default class MjmlService { } catch (error) { console.warn(error); } + return html; } - /** * Transform MJML to HTML via endpoint * @returns string */ + + static mjmlToHtmlViaEndpoint(mjml, endpoint) { const xmlHttpRequest = new XMLHttpRequest(); xmlHttpRequest.open('POST', endpoint, false); @@ -81,4 +89,5 @@ export default class MjmlService { })); return xmlHttpRequest.responseText ? JSON.parse(xmlHttpRequest.responseText) : ''; } + } \ No newline at end of file diff --git a/src/buttons.js b/src/buttons.js index 2ce7899..409ce7d 100644 --- a/src/buttons.js +++ b/src/buttons.js @@ -90,7 +90,6 @@ export default (editor, opts = {}) => { btnPreview.addCommand(); btnPreview.addButton(); - // add editor apply button const btnApply = new ButtonApply(editor); btnApply.addCommand(); @@ -136,8 +135,8 @@ export default (editor, opts = {}) => { // Add Settings Sector const traitsSector = $( '
' + - '
Settings
' + - '
' + '
Settings
' + + '' ); const traitsProps = traitsSector.find('.gjs-sm-properties'); diff --git a/src/dynamicContent/dynamicContent.domcomponents.js b/src/dynamicContent/dynamicContent.domcomponents.js index 02e551e..c460eb9 100644 --- a/src/dynamicContent/dynamicContent.domcomponents.js +++ b/src/dynamicContent/dynamicContent.domcomponents.js @@ -10,66 +10,61 @@ export default class DynamicContentDomComponents { const tagName = ContentService.isMjmlMode(editor) ? 'mj-text' : 'div'; const baseType = dc.getType(baseTypeName); const baseModel = baseType.model; - const model = baseModel.extend( - { - defaults: { - ...baseModel.prototype.defaults, - name: 'Dynamic Content', - tagName, - draggable: '[data-gjs-type=cell],[data-gjs-type=mj-column]', - droppable: false, - editable: false, - stylable: false, - propagate: ['droppable', 'editable'], - attributes: { - 'data-gjs-type': 'dynamic-content', // Type for GrapesJS - 'data-slot': 'dynamicContent', // used to find the DC component on the canvas for e.g. token transformation - }, + const model = { + defaults: { + name: 'Dynamic Content', + tagName, + draggable: '[data-gjs-type=cell],[data-gjs-type=mj-column]', + droppable: false, + editable: false, + stylable: false, + propagate: ['droppable', 'editable'], + attributes: { + 'data-gjs-type': 'dynamic-content', // Type for GrapesJS + 'data-slot': 'dynamicContent', // used to find the DC component on the canvas for e.g. token transformation }, - /** - * Initilize the component - */ - init() { - // link component to the corresponding html store item - this.em - .get('Commands') - .run('preset-mautic:link-component-to-store-item', { component: this }); + }, + /** + * Initilize the component + */ + init() { + // link component to the corresponding html store item + this.em + .get('Commands') + .run('preset-mautic:link-component-to-store-item', { component: this }); - // Add toolbar edit button if it's not already in - const toolbar = this.get('toolbar'); - const id = 'toolbar-dynamic-content'; + // Add toolbar edit button if it's not already in + const toolbar = this.get('toolbar'); + const id = 'toolbar-dynamic-content'; - if (!toolbar.filter((tlb) => tlb.id === id).length) { - toolbar.unshift({ - id, - command: 'preset-mautic:dynamic-content-open', - attributes: { class: 'fa fa-pencil-square-o' }, - }); - } - }, - // @todo: show the store items default content on the canvas - // updated(property, value, prevValue) { - // console.log('Local hook: model.updated', { - // property, - // value, - // prevValue, - // }); - // }, + if (!toolbar.filter((tlb) => tlb.id === id).length) { + toolbar.unshift({ + id, + command: 'preset-mautic:dynamic-content-open', + attributes: { class: 'fa fa-pencil-square-o' }, + }); + } }, - { - // Dynamic Content component detection - isComponent(el) { - if (el.getAttribute && el.getAttribute('data-slot') === 'dynamicContent') { - return { - type: 'dynamic-content', - }; - } - return false; - }, - } - ); + // @todo: show the store items default content on the canvas + // updated(property, value, prevValue) { + // console.log('Local hook: model.updated', { + // property, + // value, + // prevValue, + // }); + // }, + // Dynamic Content component detection + isComponent(el) { + if (el.getAttribute && el.getAttribute('data-slot') === 'dynamicContent') { + return { + type: 'dynamic-content', + }; + } + return false; + }, + }; - const view = baseType.view.extend({ + const view = { attributes: { style: 'pointer-events: all; display: table; width: 100%;user-select: none;', }, @@ -98,7 +93,7 @@ export default class DynamicContentDomComponents { // .get('Commands') // .run('preset-mautic:dynamic-content-delete-store-item', { component }); // }, - }); + }; // add the Dynamic Content component dc.addType('dynamic-content', {