Is it possible to integrate Polymer elements/WebComponents into TinyMCE?

I've been working on creating a unique TinyMCE editor for educational content, aiming to wrap specific blocks as interactive 'activities'. Each block of content will contain multiple activities with their own primary keys for identification.

The main obstacle I'm facing is developing a plugin to facilitate this functionality. While shortcodes are an option, they tend to cause errors. I have explored the possibility of using custom HTML tags rendered through Polymer. Can this approach be successfully implemented?

Answer №1

For a more up-to-date solution, here is an ES6 module and standard Web component implementation that differs from @liamzebedee's outdated solution.

The main change in this code is the script injection into the TinyMCE <iframe/> to define the Web component. Despite this alteration, the accepted tag remains inert.

Below is the snippet related to initializing 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,
    extended_valid_elements: `${name}[${attribute}]`,

    init_instance_callback: function(editor) {
        const edDoc = editor.getDoc();
        const scriptTag = edDoc.createElement('script');
        scriptTag.src = definitionFile;
        scriptTag.type = 'module';
        edDoc.querySelector('head').appendChild(scriptTag);
    }
});

Here is the basic implementation of the corresponding Web component:

export class SimpleNumber extends HTMLElement {

    constructor() {
        super();

        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" />
        `;

        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);
}

Please note that I am unable to provide a live demo on platforms like codepen.io due to the requirement for a separate JS file for the custom element definition that needs to be injected into the <iframe/>.

Answer №2

It took me a good 4 hours to figure it all out.

To properly set up the TinyMCE editor with custom elements, you need to initialize it like this:

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

The function registerCustomWebComponents should be defined as follows:

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

I found it best to define the custom HTML element and React component separately rather than constructing the HTML as a string.

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);
  }
}

Answer №3

Avoid using tinymce in polymer as it heavily relies on the document root, which can be broken by shadow dom.

However, there is a solution...

Utilize an object in your polymer template to load tinymce and resolve the document root issue. Once loaded, you can access tinymce from the object in the following manner.

Create an HTML file to load 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>

Add an object to your component template and use the data attribute of the object to load the HTML content.

Once loaded, access the object, query the DOM, retrieve the textarea, add event listeners for custom save events, pre-set content, adjust height, etc. Be cautious about cross-domain issues, although serving alongside other components should work fine.

Include the object in your component template

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

Pre-load, retrieve content, set height, and handle save events

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 once loaded
        window.clearInterval(interval);

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

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

            // listen for save events every few seconds, with a 4-second debounce on save naturally
            ta.addEventListener('save', (ev) => {
                this.set('element.value', ta.tinymce.activeEditor.getContent({ format: 'raw' }));
            });
        }, 250);
    }, 250);
}

This approach works smoothly for polymer 3 and tinymce, providing fast loading, auto-resizing, and efficient handling of save events without altering tinymce's default setup. You can apply a similar method to circumvent shadow dom limitations in other embedded applications.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using JavaScript to implement Gzip compression

As I develop a Web application that must save JSON data in a limited server-side cache using AJAX, I am facing the challenge of reducing the stored data size to comply with server quotas. Since I lack control over the server environment, my goal is to gzip ...

Utilize AngularJS to inject a service into a module without assigning it to a variable, enabling easier minification

Currently, I am attempting to decrease the size of my AngularJS JavaScript code (using SquishIt). Within my module, there is a service injected as a function argument, as shown below. var myapp = angular.module('myapp', ['ngSanitize'] ...

Encountering the error message "Uncaught TypeError: $.ajax is undefined"

Recently, I encountered an issue with my form that utilizes ajax to send user information to a php file. The form is embedded within a bootstrap modal and was functioning perfectly until I attempted to add an extra field for enhanced functionality. However ...

How to modify a variable in the Config.json using a Discord.js command

Lately, I enhanced my bot's functionality by allowing it to retrieve the color for embeds from a file specified in my config.json. All I need to do is modify something like: "embedcolor": "00A950" to "embedcolor": "0 ...

Utilizing ProtractorJS to Extract Numbers from Text within an Element and Dynamically Adding it to an Xpath Expression

Situation My objective is to extract text from an element on a webpage, convert that extracted text into a number in string format, and then use it for an xpath query. The code snippet below illustrates this process: var bookingRefString = element(by.css ...

Uncovering the Image Orientation in Angular: Is it Possible to Determine the Direction Post-view or Upon Retrieval from Database?

I am currently working on creating centered and cropped thumbnails for images retrieved from a database. I came across some helpful information on how to achieve this: The resource I found is written for JavaScript, but I am using Angular 7. I am facing d ...

What is the maximum size limit for a MongoDB array when using $addToSet?

Need to: db.collection('users').update( { "_id": user._id}, { "$addToSet": { "keywords.RFD": keywords } }, function(err, result) { if (err) { console.log( 'failed to add keyword "%s" for ...

A Foolproof Method to Dynamically Toggle HTML Checkbox in ASP.NET Using JavaScript

Although there have been numerous inquiries related to this subject, I haven't been able to find a solution specific to my situation. I currently have a gridview that contains checkboxes. What I'm trying to achieve is that when I select the chec ...

Alert: Using Angularfire SDK could result in console log issues

I've been encountering this persistent warning that I just can't seem to get rid of no matter what I try. Here are the versions I'm using: angular 11.0.1 @angular/fire 6.1.3 firebase 7.0.0 || 8.0.0 https://i.sstatic.net/5Tyt5.png ...

Tips for patiently waiting for a series of asynchronous calls to successfully complete

I have a scenario where I am dealing with multiple asynchronous calls that are dependent on each other's success. For example: function1 = () => { /*Some required Logic*/ return fetch("myurl") .then((json) => { functi ...

Utilizing JavaScript to adjust the width of an HTML element

Struggling to manipulate the position and size of a div tag using a JavaScript function, particularly confused about how to retrieve the current width of the div. Here is the function in question function socialExt() { document.getElementById("Left") ...

Challenge with URL paths in ReactJS

My webserver is up and running with all the http endpoints linked to the base URL: http://<some_name>/widget/ On the frontend, I have a ReactJS app. The issue arises after building the ReactJS app, as the built index.html references the following U ...

The renderToString function in Material UI's sx property doesn't seem to have

Every time I apply sx to a component that is rendered as a string and then displayed using dangerouslySetInnerHtml, the styles within the sx prop do not work. Here is an example showcasing the issue: Codesandbox: https://codesandbox.io/p/sandbox/wonderfu ...

I am looking to implement a post request feature in JavaScript and HTML that is similar to the functionality in my Python code

Although I am proficient in Python, I am struggling with JavaScript while trying to create an HTML page. I have a Python code snippet that I would like to replicate in JS. Can anyone provide some assistance? :) Here is my Python Code: import requests pos ...

How can I set a background image to automatically adjust to the width of the window, be full height, allow for vertical scrolling, and

How can I set a full-screen background image that adjusts to the body width without horizontal scrolling, maintains height proportionate to width, and allows for vertical scrolling? Here is my current code: html, body { margin: 0px; padding: 0px; } bo ...

Display a modal popup form in ReactJS when a particular key is pressed

As a beginner in ReactJS, I am currently developing the frontend of a web application that requires multiple modal dialogues to be displayed based on specific key combinations. To achieve this functionality, I plan to utilize JQuery-UI for the modal dialog ...

Injecting data into a Q promise

I'm facing some issues related to what seems like JavaScript closures. In my Express + Mongoose web application, I am utilizing the Q library for Promises. I have a question regarding passing request data to the promise chain in order to successfully ...

Is it possible to determine which child element is currently in view within a scrollable parent div?

In an attempt to replicate a "current page" feature using divs, similar to a PDF reader. document.addEventListener("DOMContentLoaded", function(event) { var container = document.getElementById("container"); container.onscroll = function() { let ...

Explanation of Default Export in TypeScript

I recently started learning about JS, TS, and node.js. While exploring https://github.com/santiq/bulletproof-nodejs, I came across a section of code that is a bit confusing to me. I'm hoping someone can help explain a part of the code. In this project ...

Deliver a universal Angular application using NodeJS and ExpressJS for the server-side functionality

I have set up a Node/Express JS back-end on an AWS instance using a Linux image, along with NGINX as the web server. Currently, my MEAN application is running smoothly as I work on building the Angular application. I copy the dist folder to the Node/Expres ...