codex-team / editor.js

A block-style editor with clean JSON output
https://editorjs.io
Apache License 2.0
27.59k stars 2.02k forks source link

Unable to use inside Shadow Dom and Webcomponents #1009

Closed andrewebdev closed 2 years ago

andrewebdev commented 4 years ago

We wanted to use EditorJS with customElements (we use lit-html internally). Our reasons behind this is because we wanted to isolate the editor styles away from the main dom so that our CMS styles don't make the editor content look strange. This way we can also customize the CSS nicely so that the end user looks at the formatting and text with about the same styles as they would on the frontend.

We previously used a Iframe for this (which still works), but we would like to avoid that if possible.

With the editor in the customElement, we get errors like these: editorjs_shadowdom

Here is the code that creates the customElement:

import '@editorjs/editorjs/dist/editor.js';
import {LitElement, html, css} from 'lit-element';

class HTMLEngine {
  constructor(inputEl, templateSelector, editorConfig) {
    this.inputEl = inputEl;
    this.editorConfig = editorConfig;
  }

  read() {
    let doc = new DOMParser().parseFromString(this.inputEl.value, 'text/html');
    var dataTemplate = doc.querySelector('[data-editor-data]');
    if (dataTemplate) {
      var jsonData = dataTemplate.dataset.editorData;
      return JSON.parse(jsonData);
    }
    return null;
  }

  write(outputData) {
    var editorData = JSON.stringify(outputData);
    var renderedBlocks = '';
    outputData.blocks.forEach((block) => {
      renderedBlocks += this.renderBlock(block);
    });
    this.inputEl.value = `<template data-editor-data='${editorData}'></template>
      ${renderedBlocks}`;
  }

  renderBlock(block) {
    return this.editorConfig.tools[block.type].HTMLGenerator(block.data) + '\n';
  }
}

class OstinatoEditorWidget extends LitElement {
  static get properties() {
    return {
      saveTo: { type: String },
      editorConfig: { type: String },
      editorFramePath: { type: String },
    }
  }

  constructor() {
    super();
    this.saveTo = '';
    this.editorConfig = '';
    this.editorFramePath = '';
  }

  connectedCallback() {
    super.connectedCallback();
    this.saveToEl = document.querySelector(this.saveTo);
    // Now import our editor config.
    import(this.editorConfig).then((m) => {
      this.config = m.editorConfig;
      this.engine = new HTMLEngine(
        this.saveToEl,
        '[editorjs-data]',
        this.config);
      this.initEditor();
    });
  }

  initEditor() {
    this.config.data = this.engine.read();
    this.config.holder = this.shadowRoot.getElementById('editor');

    this.config.onChange = function() {
      this.editor.save().then((outputData) => {
        if (outputData) this.engine.write(outputData);
      });
    }.bind(this);

    console.log(this.config);

    this.editor = new EditorJS(this.config);
  }

  static get styles() {
    return css`
      :host {
        display: block;
        width: 100%;
      }

      #editor {
        width: 100%;
        box-shadow: 0 0 1px 2px #e3e3e3;
      }
    `;
  }

  render() {
    return html`
      <div id="editor"></div>
    `;
  }
}

customElements.define('ostinato-editor-widget', OstinatoEditorWidget);

And this is how we are using the element:

{% load staticfiles %}

<textarea name="{{ widget.name }}"
  {% include "django/forms/widgets/attrs.html" %}
  style="display: none;">{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

<ostinato-editor-widget
  saveTo='[name="{{ widget.name }}"]'
  editorConfig="{{ config_module }}">
</ostinato-editor-widget>

Edit: Oh and here is the config that is used.

export const editorConfig = {
  initialBlock: "paragraph",
  minHeight: "400px",
  autoFocus: true,

  tools: {
    header: {
      class: Header,
      config: {
        placeholder: 'Header Text'
      },
      shortcut: 'CMD+SHIFT+H',
      HTMLGenerator: (data) => `<h${data.level}>${data.text}</h${data.level}>`,
    },

    paragraph: {
      class: Paragraph,
      shortcut: 'CMD+SHIFT+P',
      HTMLGenerator: (data) => `<p>${data.text}</p>`,
    },

    list: {
      class: List,
      inlineToolbar: true,
      shortcut: 'CMD+SHIFT+L',
      HTMLGenerator: (data) => {
        let tagname = data.style.charAt(0) + 'l';
        var renderItem = (item) => { return `<li>${item}</li>`; }
        var items = '';
        data.items.forEach((item) => { items += renderItem(item); });
        return `<${tagname}>${items}</${tagname}>`;
      },
    },

    quote: {
      class: Quote,
      inlineToolbar: true,
      config: {
        quotePlaceholder: 'Enter a quote',
        captionPlaceholder: 'Caption or Author',
      },
      shortcut: 'CMD+SHIFT+O',
      HTMLGenerator: (data) => {
        return `<blockquote style="quote-${data.alignment}">
          <p class="quote-text">${data.text}</p>
          <p class="quote-caption">${data.caption}</p>
        </blockquote>`;
      },
    },

    warning: {
      class: Warning,
      inlineToolbar: true,
      shortcut: 'CMD+SHIFT+W',
      config: {
        titlePlaceholder: 'Warning Title',
        messagePlaceholder: 'Warning Message',
      },
      HTMLGenerator: (data) => {
        return `<div class="warning">
            <p class="warning-title">${data.title}</p>
            <p class="warning-message">${data.message}</p>
          </div>`;
      },
    },

    marker: {
      class:  Marker,
      shortcut: 'CMD+SHIFT+M'
    },

    code: {
      class:  CodeTool,
      shortcut: 'CMD+SHIFT+C',
      HTMLGenerator: (data) => { return `<code>${data.code}</code>`; }
    },

    delimiter: {
      class: Delimiter,
      HTMLGenerator: () => { return `<div class="ce-delimiter"></div>` }
    },

    inlineCode: {
      class: InlineCode,
      shortcut: 'CMD+SHIFT+C'
    },

    table: {
      class: Table,
      inlineToolbar: true,
      shortcut: 'CMD+ALT+T',
      HTMLGenerator: (data) => {
        var rows = '';
        data.content.forEach((row) => {
          var cells = '';
          row.forEach((cell) => { cells += '<td>' + cell + '</td>'; })
          rows += '<tr>' + cells + '</tr>';
        });
        return '<table>' + rows + '</table>';
      }
    },
  },
};
gohabereg commented 4 years ago

Hi @andrewebdev I don't see tools imported. Are they imported somewhere else?

andrewebdev commented 4 years ago

yeah I just snipped that part out of the paste. All imports are working. The exact same config works in our iFrame version of the code, but as I mentioned we would like to transition away from the iframe requirement.

gohabereg commented 4 years ago

Could you provide Editor.js version also

It seems something wrong with internal modules import

andrewebdev commented 4 years ago

@gohabereg aha, you just made me think to look at something. Not all my modules are packaged for web, so some have relative imports which might then break the internal imports for other modules. I'll run everything through rollup and revert back with results.

In the meantime, if still interested, here is the versions:

  "dependencies": {
    "@editorjs/checklist": "^1.1.0",
    "@editorjs/code": "^2.4.1",
    "@editorjs/delimiter": "^1.1.0",
    "@editorjs/editorjs": "^2.16.1",
    "@editorjs/embed": "^2.2.1",
    "@editorjs/header": "^2.3.2",
    "@editorjs/inline-code": "^1.3.1",
    "@editorjs/list": "^1.4.0",
    "@editorjs/marker": "^1.2.2",
    "@editorjs/quote": "^2.3.0",
    "@editorjs/table": "^1.2.1",
    "@editorjs/warning": "^1.1.1",
    "@editorjs/paragraph": "^2.6.1",
    "lit-element": "^2.0.0-rc.3",
    "lit-html": "^1.0.0-rc.2"
  },
  "devDependencies": {
    "rollup": "^1.1.0",
    "rollup-plugin-node-resolve": "^4.0.0"
  }
andrewebdev commented 4 years ago

@gohabereg ok so it seems id didn't just have anything to do with the tools. I attempted to use it without specifying any tools in my config, just to test. And the editor still doesn't load.

editorjs_shadowdom2

andrewebdev commented 4 years ago

Ah @gohabereg I see the problem. It seems like, importing the editor will add the CSS to the main document, which of course wont leak through into the shadow dom of our custom element. Are the CSS exported in the main module, then I can maybe import it and insert it into the custom element.

katsew commented 4 years ago

I'm having the same issue. I made a CodePen to reproduce this.

I've tried to render styles into shadowRoot by patching loadStyles in ui.ts, but it seems that it isn't enough;( It would be nice to support that render the editor into the shadowRoot. I want to separate the scope of css from other css frameworks.

andrewebdev commented 4 years ago

@katsew I also discovered that every plugin also injects their own CSS into the main document as well. So every one of the plugins would also need to allow for usage inside the shadowroot, which is quite annoying.

rrrepos commented 3 years ago

I am facing the same issues. Any solution folks? Any suggested direction? Is there a manner in which the css can be appended to the webcomponent? Thanks

aletorrado commented 3 years ago

Any update on this?

Btw, it's specially annoying that plugins doesn't use any encapsulation nor namespacing for class names, so it's very easy to mess up. Now that Shadow Dom is widely supported across all major browsers, it would be awesome to embrace it for editor.js internals as well!

kevinxqi commented 3 years ago

Any update?

mbolli commented 2 years ago

Yep, came here while trying to build a web component with editor.js, but with CSS in the wrong DOM it's not feasible. Another problem is that CSS is not distributed in the packages (except in the js bundle file), so can't incorporate it in the build process either.

akronbook commented 2 years ago

I found an easy workaround to be able to use editorJS inside a web component. When designing your web component with your favorite framework (e.g. stencilJS, LitElement, Fast Element, etc.), create a as a placeholder for the editor. Since slot can be styled from outside (e.g. via a Document-level CSS), EditorJS will just work inside the web component.

Here is some pseudo code for your reference: // Web component definition @customElement('my-element') class MyComponent extends LitElement { render() { return html''; // Embed slot inside your HTML } }

In your index.html, make editorjs DIV a child element of my-element, and add the following script: var editor = new EditorJS({holder: 'editorjs',....});

I hope this is helpful to you!