GrapesJS / mjml

Newsletter Builder with MJML components in GrapesJS
http://grapesjs.com/demo-mjml.html
BSD 3-Clause "New" or "Revised" License
624 stars 222 forks source link

Invalid Target Po #294

Closed Drew-Daniels closed 1 year ago

Drew-Daniels commented 1 year ago

I found this issue detailing the same warning that I am seeing now, only this appears to be caused after upgrading from grapesjs 0.18.4 to 0.19.5:

The Bug:

The set up below worked prior to upgrading grapesjs from 0.18.4 to 0.19.5, but no longer works. I am getting this warning when I attempt to drag any block item onto the canvas:

image

Code:

defaults.js:

import grapesJSMJML from 'grapesjs-mjml';

const defaults = {
  container: '#gjs',
  height: '600px',
  width: 'auto',
  fromElement: true,

  plugins: [grapesJSMJML],
  pluginsOpts: {
    [grapesJSMJML]: {
      cmdBtnMoveLabel: 'Move',
      cmdBtnUndoLabel: 'Undo',
      cmdBtnRedoLabel: 'Redo',
      cmdBtnDesktopLabel: 'Desktop',
      cmdBtnTabletLabel: 'Tablet',
      cmdBtnMobileLabel: 'Mobile',

      expTplBtnTitle: 'View Code',
      fullScrBtnTitle: 'FullScreen',
      swichtVwBtnTitle: 'View Components',
      defaultTemplate: '', // Default template in case the canvas is empty
      categoryLabel: 'Basic',
    },
  },
  storageManager: {
    id: '', // 'gjs-',             // Prefix identifier that will be used on parameters
    type: 'remote', // Type of the storage
    autosave: true, // Store data automatically
    autoload: true, // Autoload stored data on init
    stepsBeforeSave: 1, // If autosave enabled, indicates how many changes are necessary before store method is triggered
  },
  colorPicker: {
    appendTo: '#picker-container',
  },
  blockManager: {
    appendTo: '#mjml-editor-blocks',
  },
  deviceManager: {
    devices: [
      {
        name: 'Desktop',
        width: '', // default size
      },
      {
        name: 'Mobile',
        width: '320px', // this value will be used on canvas width
        widthMedia: '480px', // this value will be used in CSS @media
      },
    ],
  },
  traitManager: {
    appendTo: '.styles-container',
  },
  styleManager: {
    appendTo: '.styles-container',
    sectors: [
      {
        name: 'Dimension',
        open: false,
        // Use built-in properties
        buildProps: ['width', 'min-height', 'padding'],
        // Use `properties` to define/override single property
        properties: [
          {
            // Type of the input,
            // options: integer | radio | select | color | slider | file | composite | stack
            type: 'integer',
            name: 'The width', // Label for the property
            property: 'width', // CSS property (if buildProps contains it will be extended)
            units: ['px', '%'], // Units, available only for 'integer' types
            defaults: 'auto', // Default value
            min: 0, // Min value, available only for 'integer' types
          },
        ],
      },
    ],
  },
  panels: {
    defaults: [
      {
        id: 'layers',
        el: '.panel__right',
        // Make the panel resizable
        resizable: {
          maxDim: 350,
          minDim: 200,
          tc: 0, // Top handler
          cl: 1, // Left handler
          cr: 0, // Right handler
          bc: 0, // Bottom handler
          // Being a flex child we need to change `flex-basis` property
          // instead of the `width` (default)
          keyWidth: 'flex-basis',
        },
      },
      {
        id: 'panel-devices',
        el: '.panel__devices',
        buttons: [
          {
            id: 'device-desktop',
            className: 'fa fa-desktop',
            command: 'set-device-desktop',
            active: true,
            togglable: false,
          },
          {
            id: 'device-mobile',
            className: 'fa fa-mobile',
            command: 'set-device-mobile',
            togglable: false,
          },
        ],
      },
      {
        id: 'panel-options',
        el: '#panel__options',
        buttons: [
          {
            id: 'undo',
            className: 'fa fa-undo',
            command: 'undo',
          },
          {
            id: 'redo',
            className: 'fa fa-repeat',
            command: 'redo',
          },
          {
            id: 'mjml-import',
            className: 'fa fa-upload',
            command: 'mjml-import',
          },
        ],
      },
      {
        id: 'basic-actions',
        el: '.panel__basic-actions',
        buttons: [
          {
            id: 'visibility',
            active: true, // active by default
            className: 'fa fa-square-o',
            command: 'sw-visibility', // Built-in command
          },
          {
            id: 'preview',
            className: 'fa fa-eye',
            command: 'preview',
            context: 'preview',
            attributes: { title: 'Preview' },
          },
          {
            id: 'export',
            className: 'fa fa-download',
            command: 'export-template',
            context: 'export-template', // For grouping context of buttons from the same panel
          },
          {
            id: 'show-json',
            className: 'fa fa-code',
            context: 'show-json',
            command(editor) {
              editor.Modal.setTitle('Components JSON')
                .setContent(
                  `<textarea style="width:100%; height: 450px;">
                ${JSON.stringify(editor.getComponents())}
              </textarea>`,
                )
                .open();
            },
          },
        ],
      },
    ],
  },
};

export default defaults;

Ged.js (i.e., grapesJS Editor)

import GrapesJS from 'grapesjs';
import ReactDOMServer from 'react-dom/server';
import React from 'react';
import defaultsDeep from 'lodash/defaultsDeep';
import defaults from './defaults';

/* eslint-disable */
class Ged {
  constructor(id, { variables, linkVariables }) {
    const accessToken = localStorage.getItem('<localStorageKeyHere>');
    const config = defaultsDeep(
      {
        storageManager: {
          options:
          {
            remote:
            {
              headers: {
                Authorization: `Bearer ${accessToken}`,
              },
              urlStore: `${REACT_APP_API_URL}/api/email_templates/${id}/builder_store`,
              urlLoad: `${REACT_APP_API_URL}/api/email_templates/${id}/builder_load`,
              contentTypeJson: true,
              credentials: 'same-origin',
              onStore: (data, editor) => {
                const pagesHtml = editor.Pages.getAll().map(page => {
                  const component = page.getMainComponent();
                  return {
                    html: editor.getHtml({ component }),
                    css: editor.getCss({ component }),
                  }
                });
                return {
                  ...data, ...pagesHtml[0]
                };
              },
              onLoad: result => {
                return {...result, pages : [{ component: result.html }]};
              },
            },
          }
        },
        richTextEditor: {
          actions: this.getActions(variables),
        },
      },
      defaults,
    );
    if (Ged.editor) {
      Ged.editor.destroy();
    }
    Ged.editor = GrapesJS.init(config);
    Ged.editor.Components.addType('wrapper', {
      model: {
        defaults: {
          tagName: 'div', // replace body to div
        },
        // And/or skip wrapper in the HTML output
        toHTML: function(opts) {
          return this.getInnerHTML(opts);
        }
      }
    });
    this.removeDevicePanel();
    this.addDeviceCommands();
    this.updateImage();
    this.addVariables(linkVariables);
    this.addBlocks();
    this.removeBlocks();
  }

  removeBlocks() {
    const { editor } = Ged;
    editor.BlockManager.remove('mj-hero');
    editor.BlockManager.remove('mj-wrapper');
  }

  addBlocks() {
    const { editor } = Ged;
    editor.BlockManager.add('a', {
      label: 'Link',
      content: '<a class="link">Select to change the URL</a>',
      attributes: { class: 'fa fa-link' },
    });
    editor.DomComponents.addType('link', {
      model: {
        defaults: {
          draggable: '[data-gjs-type=mj-text]',
        },
      },
    });
    editor.DomComponents.addType('mj-text', {
      model: {
        defaults: {
          droppable: ['a'],
        },
      },
    });
  }

  updateImage() {
    const comps = Ged.editor.DomComponents;
    const originalImage = comps.getType('mj-image');
    comps.addType('mj-image', {
      model: {
        defaults: {
          traits: ['href', 'rel', 'alt', 'title', 'src'],
          void: false,
        },
      },
    });
  }

  getActions(variables) {
    const { editor } = Ged;
    const selectDropdown = (
      <select className="gjs-field">
        <option value="" disabled selected>
          Variables
        </option>
        {variables.map((option) => (
          <option key={option.key} value={`{{ ${option.key} }}`}>
            {option.display_name}
          </option>
        ))}
      </select>
    );
    const actions = [
      'bold',
      'italic',
      'underline',
      'strikethrough',
      'link',
      {
        name: 'vars',
        icon: ReactDOMServer.renderToString(selectDropdown),
        // Bind the 'result' on 'change' listener
        event: 'change',
        result: (rte, action) => rte.insertHTML(action.btn.firstChild.value),
        // Reset the select on change
        update: (rte, action) => {
          action.btn.firstChild.value = '';
        },
      },
    ];

    return actions;
  }

  removeDevicePanel() {
    const { editor } = Ged;
    editor.Panels.removePanel('devices-c');
  }

  addDeviceCommands() {
    const { editor } = Ged;

    editor.Commands.add('set-device-desktop', {
      run: (editor) => editor.setDevice('Desktop'),
    });

    editor.Commands.add('set-device-mobile', {
      run: (editor) => editor.setDevice('Mobile'),
    });
  }

  addVariables(variables) {
    const { editor } = Ged;

    const buttonOptions = variables.map((option) => ({
      value: `{{ ${option.key}}}`,
      name: option.display_name,
    }));

    editor.DomComponents.addType('mj-button', {
      model: {
        defaults: {
          traits: [
            {
              type: 'select',
              label: 'Link',
              name: 'href',
              options: buttonOptions,
            },
          ],
        },
      },
    });
  }
}

export default Ged;

Builder.js:

import React from 'react';
import isEmpty from 'lodash/isEmpty';
import 'grapesjs/dist/css/grapes.min.css';
import './geditor.less';
import Ged from './Ged';

const { useEffect, useState } = React;

// slim example from https://github.com/thanhtunguet/grapesjs-react/issues/12
function Builder(props) {
  const { id, variables, linkVariables } = props;
  const [editor, setEditor] = useState(null);
  useEffect(() => {
    if (!editor && !isEmpty(variables) && !isEmpty(linkVariables)) {
      const newEditor = new Ged(id, { variables, linkVariables });
      setEditor(newEditor);
    }
  }, []);

  return (
    <div id="mjml-editor">
      <div className="mjml-editor-row" style={{ height: '100%' }}>
        <div id="mjml-editor-layers" className="mjml-editor-column" style={{ flexBasis: '200px' }}>
          <div id="panel__options" />
          <br />
          <br />
          <div id="mjml-editor-blocks" />
        </div>
        <div className="mjml-editor-column mjml-editor-clm">
          <div className="panel__top">
            <div className="panel__basic-actions" />
            <div className="panel__devices" />
            <div className="panel__switcher" />
          </div>
          <div className="editor-row">
            <div className="editor-canvas">
              <div id="gjs" style={{ overflow: 'hidden', height: '100%' }}>
                <mjml>
                  <mj-body>
                    <mj-section></mj-section>
                  </mj-body>
                </mjml>
              </div>
              <div id="picker-container" className="gjs-editor-cont" />
            </div>
            <div className="panel__right">
              <div className="layers-container" />
              <div className="styles-container" style={{ overflow: 'auto' }} />
              <div className="traits-container" />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default Builder;

What I have tried:

Previously:

Builder.js:

<div id="gjs" style={{ overflow: 'hidden', height: '100%' }}>
  <mjml>
    <mj-body>
      <mj-section></mj-section>
    </mj-body>
  </mjml>
</div>

Ged.js:

...
Ged.editor = GrapesJS.init(config);
Ged.editor.Components.addType('wrapper', {
  model: {
    defaults: {
      tagName: 'div', // replace body to div
    },
    // And/or skip wrapper in the HTML output
    toHTML: function(opts) {
      return this.getInnerHTML(opts);
    }
  }
});
...

Currently:

Builder.js:

...
<div id="gjs" style={{ overflow: 'hidden', height: '100%' }} />
...

Ged.js:

...
Ged.editor = GrapesJS.init(config);
Ged.editor.Components.addType('wrapper', {
  model: {
    defaults: {
      tagName: 'div', // replace body to div
    },
    // And/or skip wrapper in the HTML output
    toHTML: function(opts) {
      return this.getInnerHTML(opts);
    }
  }
});
Ged.editor.addComponents('<mjml><mj-body><mj-section></mj-section></mj-body></mjml>')
...