我可以将 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/>
)…
我正在尝试构建一个用于教学内容的自定义 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/>
)…