Blog: JS

TinyMCE 5: Creating a custom Dialog Plugin (and with Custom Button Icons)

Published

Edited: February 6, 2019. The included code also works with the TinyMCE 5 Stable release.

I’ve been using TinyMCE for years – I think if I look back in my code history, v2, maybe even v1. And it’s such a brilliant WYSIWYG editor.  

In this article, I'll be showing you how to:

  1. Get started with a basic TinyMCE 5 Plugin
  2. Make your Plugin have a Dialog using TinyMCE 5's UI components
  3. Update the Dialog and its data dynamically after instantiation
  4. Adding custom SVG icons for your Plugin's Button and Menu Item

I’ve spent a few days this week playing with the Release Candidate for TinyMCE 5, and specifically looking at how my old v3/v4 plugins can work within the new framework.

v2 to v3 improved the user interface. v3 to v4 really improved the user interface. And now v5 takes it even further.

TinyMCE now feels modern and integrated in with the rest of the page’s interface. The icons (as coloured images, aka old school Word) are long gone, and now have a very clean and simple interface.

It has been great to see the evolution of mobile browser support and the user experience on these devices. Its responsiveness and access on touch devices – be that a small screen phone or a larger tablet – is now feeling intuitive and natural on a mobile device. Brilliant!

v5 feels, looks and behaves beautifully.

However, while playing with v5 and specifically looking at Dialogs and Plugins, I found out a few key points:

  1. The API creates breaking changes – v4 to v5 is not a simple upgrade, and will (in most cases) require work
  2. Adding custom icons is dramatically different, now referenced using SVGs rather than font icons
  3. iframe content within a Dialog is no longer supported

The biggest issue here is the last point – iframes. I do use one third party plugin that provides its core functionality through an iframe. This means the plugin needs to be re-written to use v5’s API to function – and that will take time.

Even though that’s an issue, there’s also a silver lining. It means any Dialog-based plugins will now also be more consistent in their user interface, and utilise the same styling, modularity and accessibility as the core v5 library. So that’s actually a good thing.

Ignoring the iframe issue for the time being, to get started with v5, I wanted to look at how I would accomplish a few key requirements:

  1. The easy one – basic form elements of different field type
  2. The harder one – how to dynamically update elements with data after instantiation
  3. The unknown one – how to use a custom icon for the button

The UI Components documentation for TinyMCE 5 is superb – go and take a read – you’ll learn so much about how the new Dialog components are built (and what they can do).

A Basic Plugin Template

Let’s start with the basics. I needed to make sure my plug in was working correctly within TinyMCE 5.

Best spot to start is to look at the source code for the provided plugins – I think “code” is a good starting point given its simplicity. Looking at the uncompiled code shows its modularisation – you could go to that level too, but for the sake of getting a simple plugin started, we’re going to keep it just that – simple.

Creating a Plugin is very similar to TinyMCE 4 – so that’s good – but there’s been a minor change to how buttons and menu items are added to the toolbar and menu respectively.

editor.addButton has been replaced with editor.ui.registry.addButton.

editor.addMenuItem has been replaced with editor.ui.registry.addMenuItem.

Besides that, everything from the TinyMCE 4 plugin remains the same. Trivial. Easy. Here’s where we are now.

(function () {
    var helloworld = (function () {
        'use strict';

        tinymce.PluginManager.add("helloworld", function (editor, url) {

            function _onAction()
            {
                // Do something when the plugin is triggered
                editor.insertContent("<p>Content added from my Hello World plugin.</p>")
            }

            // Define the Toolbar button
            editor.ui.registry.addButton('helloworld', {
                text: "Hello Button",
                onAction: _onAction
            });

            // Define the Menu Item
            editor.ui.registry.addMenuItem('helloworld', {
                text: 'Hello Menu Item',
                context: 'insert',
                onAction: _onAction
            });

            // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
            // This is optional.
            return {
                getMetadata: function () {
                    return {
                        name: "Hello World example",
                        url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
                    };
                }
            };
        });
    }());
})();

Personally, I don’t use the menu, so if you don’t either, you could leave out the addMenuItem call.

One thing to note here though is the _onAction function.

I’ve defined the function separately so that both addButton and addMenuItem can both call the same function. I don’t want to define the behaviour twice, so just making it easier here.

When you get it running in TinyMCE 5, and trigger the plug in, the new paragraph from the _onAction call gets added to the editor.

A Plugin with a Dialog

So now that we have a Plugin correctly working in TinyMCE 5, and running as we expect, let’s take it further and create a Dialog using the new UI components of TinyMCE 5.

Dialogs have a really come a long way in TinyMCE. When I first wrote my first Plugin, there was lots of trial and error due to a lack of easy to process documentation. It felt a bit “hacky” at times, but ultimately did work.

TinyMCE 5’s Dialog documentation is superb. Read it. If you’re making a Plugin with a Dialog, you must read it.

Rather than rehash the documentation, let’s extend the first example and have a Dialog appear with a Select box element.

Each Dialog needs a title, a body definition, and an array of buttons. We can do that, easy.

We’re just creating a basic Dialog, so our foundation for the Body will be a Panel. TabPanel is the other – but let’s keep it simple still.

When we declare our Body and Panel, we need an array of Items. In this example, we are simply going to have a single Select box.

We also need an array of buttons – one to close the Dialog, and one to Insert – check out the docs for configuring these – simple stuff.

We’ve also got an onSubmit function there – this is triggered with the button of type “submit” is hit.

All we need to do is update our _onAction function to open the Dialog.

function _onAction()
{
    // Open a Dialog
    editor.windowManager.open({
        title: 'Hello World Example Plugin',
        body: {
            type: 'panel',
            items: [{
                type: 'selectbox',
                name: 'type',
                label: 'Dropdown',
                items: [
                    {text: 'Option 1', value: '1'},
                    {text: 'Option 2', value: '2'}
                ],
                flex: true
            }]
        },
        onSubmit: function (api) {
            // insert markup
            editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');

            // close the dialog
            api.close();
        },
        buttons: [
            {
                text: 'Close',
                type: 'cancel',
                onclick: 'close'
            },
            {
                text: 'Insert',
                type: 'submit',
                primary: true,
                enabled: false
            }
        ]
    });
}

Running the Plugin, we get a Dialog with a single Select box. Clicking “Insert” inserts the appropriate paragraph in the editor based on the selected item.

Updating with the Dialog after instantiation

One of my big requirements is being able to update the Dialog components after instantiation.

Use case: I need the Select box to contain a list of items pulled from the server.

This is where it felt incredibly hacky with TinyMCE 4. In 4, the onPostRender option for an Item meant I could grab a hold of the item’s instance, and could then update its state and data. It worked but was never clearly documented.

In TinyMCE 5, onPostRender has been depreciated, so how can I get the Item?

Reading the documentation, I was not really sure how to do this – and part of the reason I’ve written this post – to help others. I contacted the support team who were amazing at providing input and pointing me in the right direction – and explaining some of the new Dialog API’s options.

In the docs, the “Dialog instance API” has the details.

Basically you can’t access the item’s instance directly any more, and this is by design. The Dialog API provides a “redial” method that re-creates the Dialog. This means you need to pass the configuration to the “redial” method, and it can re-create the Dialog seamlessly.

This also then shows us “block” and “unblock” methods. These are incredibly useful to prevent user input while the server-side call is taking place. We can “block” before the call is made, and then when completed, “redial” then “unblock” and we have update items in the Select box.

Awesome.

But… I love to make things easier (and write less code), so basically the issue now is to enable both the windowManager.open and redial calls to use the same base configuration.  Given the size of the configuration object, I don’t want to have to replicate that multiple times.

What I have done is grabbed the dialog instance, and dynamically updated the configuration object, so that any time I call redial, it’s able to use this always-up-to-date configuration.

In other words, a simple function - _getDialogConfig - includes the Dialog configuration, which also references a variable - _typeOptions – which is an Array and stores the options I want to appear in the Select box.

I can then update _typeOptions, and call redial on the Dialog and pass the config by calling _getDialogConfig and suddenly we have the config defined once, but able to be easily updated.

Given a real server-side call requires a backend, I’ve simulated a call in this example just using a timeout. It gets to show you the “block” behaviour, and the updating of the Select box without needing a server.

(function () {
    var helloworld = (function () {
        'use strict';

        tinymce.PluginManager.add("helloworld", function (editor, url) {

            /*
            Use to store the instance of the Dialog
             */
            var _dialog = false;

            /*
            An array of options to appear in the "Type" select box.
             */
            var _typeOptions = [];

            /**
             * Get the Dialog Configuration Object
             *
             * @returns {{buttons: *[], onSubmit: onSubmit, title: string, body: {}}}
             * @private
             */
            function _getDialogConfig()
            {
                return {
                    title: 'Hello World Example Plugin',
                    body: {
                        type: 'panel',
                        items: [{
                            type: 'selectbox',
                            name: 'type',
                            label: 'Dropdown',
                            items: _typeOptions,
                            flex: true
                        }]
                    },
                    onSubmit: function (api) {
                        // insert markup
                        editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');

                        // close the dialog
                        api.close();
                    },
                    buttons: [
                        {
                            text: 'Close',
                            type: 'cancel',
                            onclick: 'close'
                        },
                        {
                            text: 'Insert',
                            type: 'submit',
                            primary: true,
                            enabled: false
                        }
                    ]
                };
            }

            /**
             * Plugin behaviour for when the Toolbar or Menu item is selected
             *
             * @private
             */
            function _onAction()
            {
                // Open a Dialog, and update the dialog instance var
                _dialog = editor.windowManager.open(_getDialogConfig());

                // block the Dialog, and commence the data update
                // Message is used for accessibility
                _dialog.block('Loading...');

                // Do a server call to get the items for the select box
                // We'll pretend using a setTimeout call
                setTimeout(function () {

                    // We're assuming this is what runs after the server call is performed
                    // We'd probably need to loop through a response from the server, and update
                    // the _typeOptions array with new values. We're just going to hard code
                    // those for now.
                    _typeOptions = [
                        {text: 'First Option', value: '1'},
                        {text: 'Second Option', value: '2'},
                        {text: 'Third Option', value: '3'}
                    ];

                    // re-build the dialog
                    _dialog.redial(_getDialogConfig());

                    // unblock the dialog
                    _dialog.unblock();

                }, 1000);
            }

            // Define the Toolbar button
            editor.ui.registry.addButton('helloworld', {
                text: "Hello Button",
                onAction: _onAction
            });

            // Define the Menu Item
            editor.ui.registry.addMenuItem('helloworld', {
                text: 'Hello Menu Item',
                context: 'insert',
                onAction: _onAction
            });

            // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
            // This is optional.
            return {
                getMetadata: function () {
                    return {
                        name: "Hello World example",
                        url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
                    };
                }
            };
        });
    }());
})();

This is awesome.

The UI components are consistent with what TinyMCE 5 provides, it remains accessible, and just feels good.

Well, I feel good at least.

Adding custom icons to the plugin button

TinyMCE 3 and 4 both made it pretty straight forward to add additional icons to buttons. It was trivial to create a font of your icons (Icomoon is awesome for this) and include the styles with a smidge of “mce” declarations to trigger the right icons. Easy.

TinyMCE 5 now uses SVG icons. And the developers have provided an API function to add additional SVG icons for use within TinyMCE. This function is currently missing in the documentation, but does work as expected.

There is a smartly named “addIcon” method that does just that – you provide a name, and some SVG data to use for the icon.

editor.ui.registry.addIcon(name, svgData);

Getting this to function as expected was a bit of trial and error, but think I have found a handy way to add multiple icons by linking to a single external SVG that contains all of my extra icons.

To do this, I need my SVG file first. I do love icomoon, so found a few suitable icons, and downloaded the SVG versions of these, and created a single file that included them all.

The idea here is to create a <symbol> definition for each of your icons, including the path data. Take note of the ID used for each (we’ll use this next), as well as the viewBox – your SVG should tell you this. From icomoon, it does vary – sometimes 512 x 512, but a few 576 x 512. Just set the right numbers so your icons don’t get cropped.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
        <symbol id="bubbles" viewBox="0 0 576 512">
	    ...
	</symbol>
	<symbol id="books" viewBox="0 0 576 512">
	    ...
	</symbol>
	…
    </defs>
</svg>

Now that we’ve got our file, let’s add the icons:

editor.ui.registry.addIcon('bubbles', '<svg class="icon"><use xlink:href="custom-icons.svg#bubbles"></use></svg>');
editor.ui.registry.addIcon('books', '<svg class="icon"><use xlink:href="custom-icons.svg#books"></use></svg>');

This has defined our two icons – we can now reference them using “bubbles” and “books”, and each SVG data is pointing to our custom icon SVG, and referencing each symbol.

This means we can create many icons in one SVG (and one HTTP request), rather than embedding them in the JS file itself. Obviously it depends on your plugin, how you want to work, and whether it is worth it, but given I have a number of plugins, and therefore a number of icons, this is perfect.

We can now specify the icon when we call the “addButton” call.

Completed TinyMCE 5 Plugin

So let’s take a look at the finished demo.

Note: for demonstrating the demo on mobile devices, I've forced the desktop theme to be used.

While saying goodbye to iframes is bad (but also good), the new UI components in TinyMCE 5 create an accessible and consistent user experience. And that's a massive plus. 

Being able to continue to use custom icons as well as dynamically updating fields within the Dialog after instantiation is an absolute must for me - and it's been great to get my head around 5's API.

Take a look at my repository on GitHub for all of the source code to get you started: https://github.com/martyf/tinymce-5-plugin-playground

Hopefully this has helped you get your head around some of the changes introduced with TinyMCE 5.

(function () {
    var helloworld = (function () {
        'use strict';

        tinymce.PluginManager.add("helloworld", function (editor, url) {

            /*
            Add a custom icon to TinyMCE
             */
            editor.ui.registry.addIcon('bubbles', '<svg class="icon"><use xlink:href="custom-icons.svg#bubbles4"></use></svg>');

            /*
            Use to store the instance of the Dialog
             */
            var _dialog = false;

            /*
            An array of options to appear in the "Type" select box.
             */
            var _typeOptions = [];

            /**
             * Get the Dialog Configuration Object
             *
             * @returns {{buttons: *[], onSubmit: onSubmit, title: string, body: {}}}
             * @private
             */
            function _getDialogConfig()
            {
                return {
                    title: 'Hello World Example Plugin',
                    body: {
                        type: 'panel',
                        items: [{
                            type: 'selectbox',
                            name: 'type',
                            label: 'Dropdown',
                            items: _typeOptions,
                            flex: true
                        }]
                    },
                    onSubmit: function (api) {
                        // insert markup
                        editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');

                        // close the dialog
                        api.close();
                    },
                    buttons: [
                        {
                            text: 'Close',
                            type: 'cancel',
                            onclick: 'close'
                        },
                        {
                            text: 'Insert',
                            type: 'submit',
                            primary: true,
                            enabled: false
                        }
                    ]
                };
            }

            /**
             * Plugin behaviour for when the Toolbar or Menu item is selected
             *
             * @private
             */
            function _onAction()
            {
                // Open a Dialog, and update the dialog instance var
                _dialog = editor.windowManager.open(_getDialogConfig());

                // block the Dialog, and commence the data update
                // Message is used for accessibility
                _dialog.block('Loading...');

                // Do a server call to get the items for the select box
                // We'll pretend using a setTimeout call
                setTimeout(function () {

                    // We're assuming this is what runs after the server call is performed
                    // We'd probably need to loop through a response from the server, and update
                    // the _typeOptions array with new values. We're just going to hard code
                    // those for now.
                    _typeOptions = [
                        {text: 'First Option', value: '1'},
                        {text: 'Second Option', value: '2'},
                        {text: 'Third Option', value: '3'}
                    ];

                    // re-build the dialog
                    _dialog.redial(_getDialogConfig());

                    // unblock the dialog
                    _dialog.unblock();

                }, 1000);
            }

            // Define the Toolbar button
            editor.ui.registry.addButton('helloworld', {
                text: "Hello Button",
                icon: 'bubbles',
                onAction: _onAction
            });

            // Define the Menu Item
            editor.ui.registry.addMenuItem('helloworld', {
                text: 'Hello Menu Item',
                context: 'insert',
                icon: 'bubbles',
                onAction: _onAction
            });

            // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
            // This is optional.
            return {
                getMetadata: function () {
                    return {
                        name: "Hello World example",
                        url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
                    };
                }
            };
        });
    }());
})();

Blog

View all
PHP

How to easily access to Custom Fields in Joomla

Over the past few years, I’ve had to get more and more involved in developing Joomla websites. Joomla is such a powerful, flexible and user-friendly CMS to work...

Continue reading...

Movie

“Gravity” and Dolby Atmos

Gravity is one of those movies that I missed in theatres, but watched in 3D on Blu-ray when it first came out. I had just gotten a 3D TV and, hey, any disc with...

Continue reading...

Photo

Making photos fit on your website

As you know, I love my work as a web developer. I also love taking my landscape photographs. So it only seemed natural that when Mity wanted someone to write about...

Continue reading...

JS

vue-slide-up-down: helping improve accessibility

When starting out in Vue, coming from jQuery, one thing that I did miss was the jQuery slideUp and slideDown functions. They’re just so convenient, easy and effortless...

Continue reading...

I am the Development Director (and co-owner) at Mity Digital, a Melbourne-based digital agency specialising in responsive web design, custom web development and graphic design.
Mity Digital