我可以将 Polymer 元素/WebComponents 与 TinyMCE 一起使用吗?

Can I use Polymer elements / WebComponents with TinyMCE?

我正在尝试构建一个用于教学内容的自定义 TinyMCE 编辑器,它允许将某些块包装为 'activity'。一个内容块中将有多个活动,因此它们将以 ID 作为主键等。

我的挑战是实现一个允许这样做的插件——理想情况下,我会使用短代码,但它们很容易出错。我正在研究使用将通过 Polymer 呈现的自定义 HTML 标签——这可以做到吗?

我在大约 4 个小时后完全解决了。

TinyMCE 编辑器需要初始化以支持自定义元素,如下所示:

{
...
    extended_valid_elements : 'module-activity',
    custom_elements : 'module-activity',
    init_instance_callback: function(editor) {
        registerCustomWebComponents(tinymce.activeEditor.dom.doc);
    },
...
}

其中 registerCustomWebComponents 看起来像:

function registerCustomWebComponents(doc) {
  doc.registerElement('module-activity', ModuleActivityHTMLElement);  
}

我最终定义了自定义 HTML 元素,然后定义了一个 React 组件,而不是将 HTMl 构建为字符串。

class ModuleActivity extends React.Component {
  constructor(props) {
    super(props);
    this.openActivityEdit = this.openActivityEdit.bind(this);
  }

  openActivityEdit() {

  }

  render() {
    return <div>
      <h3>Module Activity</h3>
      <button onClick={this.openActivityEdit}>Edit</button>
      <div dangerouslySetInnerHTML={{__html: this.props.contentHtml }} />
    </div>;
  }
}


class ModuleActivityHTMLElement extends HTMLElement {
  attachedCallback() {
    let self = this;
    var mountPoint = document.createElement('div');
    this.createShadowRoot().appendChild(mountPoint);
    ReactDOM.render(<ModuleActivity contentHtml={self.innerHTML}/>, mountPoint);
  }
}

这里的答案是不要在 polymer 中使用 tinymce,tinymce 严重依赖文档根目录和阴影 dom 会破坏它。

但就像生活中的一切一样,一切都不会丢失...

在你的 polymer 模板中使用一个对象,让对象加载 tinymce 并解决文档根问题。一旦以这种方式加载,您就可以从对象访问 tinymce。

创建一个HTML文件来加载tinymce

<!DOCTYPE html>
<html>

<head>
    <script src="https://cloud.tinymce.com/stable/tinymce.min.js"></script>

    <style>
        html { height: 96%; }
        body { height: 96%; }
    </style>
</head>

<body>
    <textarea>Loading...</textarea>
    <script>
        var ta = document.querySelector('textarea');
        ta.tinymce = tinymce;
        var editorChangeHandler;
        tinymce.init({ selector: 'textarea', height: document.body.scrollHeight - 100, setup: function (editor) {
            editor.on('Paste Change input Undo Redo', function () {
                if (editorChangeHandler) { clearTimeout(editorChangeHandler); }
                editorChangeHandler = setTimeout(function () {
                    ta.dispatchEvent(new CustomEvent('save'));
                }, 2000);
            });
        } });
    </script>
</body>

</html>

现在你只需要在组件的模板中添加一个对象,使用对象的数据属性来加载html。

加载后,您可以访问它,查询对象 dom 并抓取文本区域,添加事件侦听器以保存自定义事件并预先设置内容,调整高度。我只测试了相同的 domain,所以请注意这可能会破坏交叉 domain,但无论如何您都想与其他组件一起使用。

将您的对象添加到您的组件模板

<object id="editor" type="text/html" data="/src/lib/tinymce/tinymce.html"></object>

以及一些预加载、抓取内容、设置高度和捕获保存的方法

ready() {
    super.ready();

    // wait five seconds before capturing input
    var interval = setInterval(() => {
        if (!this.$.editor.contentDocument.body) return;
        let ta = this.$.editor.contentDocument.body.querySelector('textarea');
        if (!ta || !ta.tinymce || !ta.tinymce.activeEditor) return;

        // clear interval now loaded
        window.clearInterval(interval);

        setTimeout(() => {
            // resize on window change
            window.addEventListener('resize', this._updateEditorSize.bind(this));

            // pre load
            ta.tinymce.activeEditor.setContent(this.element.value);

            // catch updates every few seconds, this will then have a 4 second debounce on save too naturally
            ta.addEventListener('save', (ev) => {
                this.set('element.value', ta.tinymce.activeEditor.getContent({ format: 'raw' }));
            });
        }, 250);
    }, 250);
}

这是 polymer 3 和 tinymce 的工作场景,加载速度快,自动调整大小并捕获来自对象的保存事件,我不必更改 tinymce 的默认设置。您还可以将此方法用于其他方式来绕过某些嵌入式应用程序的阴影 dom。

@liamzebedee 提供的解决方案有点过时,这里有一个 au goût du jour 的解决方案,带有 ES6 模块和标准 Web 组件。

与上述代码的主要区别是在定义 Web 组件的脚本的 TinyMCE <iframe/> 中注入。否则,接受的标签保持惰性。

首先,这是一段与TinyMCE初始化相关的代码:

const name = 'custom-element';
const attribute = 'view-mode';
const insertTag = '<custom-element view-mode="editing"></custom-element>';
const definitionFile = './custom-element.js';

tinymce.PluginManager.add(name, function(editor, url) {
    editor.ui.registry.addButton(name, {
        text: name,
        onAction: () => editor.insertContent(insertTag)
    });
});

tinymce.init({
    // ...

    custom_elements: name, // just the custom Web element names
    extended_valid_elements: `${name}[${attribute}]`, // names+attributes

    init_instance_callback: function(editor) {
        const edDoc = editor.getDoc();
        const scriptTag = edDoc.createElement('script');
        scriptTag.src = definitionFile;
        scriptTag.type = 'module';
        // Injection of the definition file in the <iframe/> document
        edDoc.querySelector('head').appendChild(scriptTag);
    }
});

对应的(最小)Web组件定义为:

export class SimpleNumber extends HTMLElement {

    constructor() {
        super();

        // Add a simple <input/> field in the ShadowRoot
        this.attachShadow({mode: 'open'}).innerHTML = `
            <style>
                :host { display:inline-block; }
                input { border: 1px solid blue; font-size: 12pt; text-align: right; }
            </style>
            <input type="number" min="0" max="10" value="0" />
        `;

        // Provide some feedback when the  <input/> field value changes
        this.shadowRoot.querySelector('input').addEventListener('change', (event) => {
            const field = event.target;
            field.style.backgroundColor = field.value === '3' ? 'lightgreen' : 'orange';
        });
    }
}

if (window.customElements && !window.customElements.get('simple-number')){
    window.customElements.define('simple-number', SimpleNumber);
}

请注意,我无法在 codepen.io 上提供 运行 示例(例如),因为我无法为自定义元素定义提供单独的 JS 文件(需要注入的文件<iframe/>)…