TinyMCE

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

Published January 17th, 2019

Important: The information in this article is over 12 months old, and may be out of date or no longer relevant.

But hey, you're here anyway, so give it a read see if it still applies to you.

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.

1(function () {
2 var helloworld = (function () {
3 'use strict';
4 
5 tinymce.PluginManager.add("helloworld", function (editor, url) {
6 
7 function _onAction()
8 {
9 // Do something when the plugin is triggered
10 editor.insertContent("<p>Content added from my Hello World plugin.</p>")
11 }
12 
13 // Define the Toolbar button
14 editor.ui.registry.addButton('helloworld', {
15 text: "Hello Button",
16 onAction: _onAction
17 });
18 
19 // Define the Menu Item
20 editor.ui.registry.addMenuItem('helloworld', {
21 text: 'Hello Menu Item',
22 context: 'insert',
23 onAction: _onAction
24 });
25 
26 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
27 // This is optional.
28 return {
29 getMetadata: function () {
30 return {
31 name: "Hello World example",
32 url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
33 };
34 }
35 };
36 });
37 }());
38})();
Copied!

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.

1function _onAction()
2{
3 // Open a Dialog
4 editor.windowManager.open({
5 title: 'Hello World Example Plugin',
6 body: {
7 type: 'panel',
8 items: [{
9 type: 'selectbox',
10 name: 'type',
11 label: 'Dropdown',
12 items: [
13 {text: 'Option 1', value: '1'},
14 {text: 'Option 2', value: '2'}
15 ],
16 flex: true
17 }]
18 },
19 onSubmit: function (api) {
20 // insert markup
21 editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');
22 
23 // close the dialog
24 api.close();
25 },
26 buttons: [
27 {
28 text: 'Close',
29 type: 'cancel',
30 onclick: 'close'
31 },
32 {
33 text: 'Insert',
34 type: 'submit',
35 primary: true,
36 enabled: false
37 }
38 ]
39 });
40}
Copied!

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.

1(function () {
2 var helloworld = (function () {
3 'use strict';
4 
5 tinymce.PluginManager.add("helloworld", function (editor, url) {
6 
7 /*
8 Use to store the instance of the Dialog
9 */
10 var _dialog = false;
11 
12 /*
13 An array of options to appear in the "Type" select box.
14 */
15 var _typeOptions = [];
16 
17 /**
18 * Get the Dialog Configuration Object
19 *
20 * @returns {{buttons: *[], onSubmit: onSubmit, title: string, body: {}}}
21 * @private
22 */
23 function _getDialogConfig()
24 {
25 return {
26 title: 'Hello World Example Plugin',
27 body: {
28 type: 'panel',
29 items: [{
30 type: 'selectbox',
31 name: 'type',
32 label: 'Dropdown',
33 items: _typeOptions,
34 flex: true
35 }]
36 },
37 onSubmit: function (api) {
38 // insert markup
39 editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');
40 
41 // close the dialog
42 api.close();
43 },
44 buttons: [
45 {
46 text: 'Close',
47 type: 'cancel',
48 onclick: 'close'
49 },
50 {
51 text: 'Insert',
52 type: 'submit',
53 primary: true,
54 enabled: false
55 }
56 ]
57 };
58 }
59 
60 /**
61 * Plugin behaviour for when the Toolbar or Menu item is selected
62 *
63 * @private
64 */
65 function _onAction()
66 {
67 // Open a Dialog, and update the dialog instance var
68 _dialog = editor.windowManager.open(_getDialogConfig());
69 
70 // block the Dialog, and commence the data update
71 // Message is used for accessibility
72 _dialog.block('Loading...');
73 
74 // Do a server call to get the items for the select box
75 // We'll pretend using a setTimeout call
76 setTimeout(function () {
77 
78 // We're assuming this is what runs after the server call is performed
79 // We'd probably need to loop through a response from the server, and update
80 // the _typeOptions array with new values. We're just going to hard code
81 // those for now.
82 _typeOptions = [
83 {text: 'First Option', value: '1'},
84 {text: 'Second Option', value: '2'},
85 {text: 'Third Option', value: '3'}
86 ];
87 
88 // re-build the dialog
89 _dialog.redial(_getDialogConfig());
90 
91 // unblock the dialog
92 _dialog.unblock();
93 
94 }, 1000);
95 }
96 
97 // Define the Toolbar button
98 editor.ui.registry.addButton('helloworld', {
99 text: "Hello Button",
100 onAction: _onAction
101 });
102 
103 // Define the Menu Item
104 editor.ui.registry.addMenuItem('helloworld', {
105 text: 'Hello Menu Item',
106 context: 'insert',
107 onAction: _onAction
108 });
109 
110 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
111 // This is optional.
112 return {
113 getMetadata: function () {
114 return {
115 name: "Hello World example",
116 url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
117 };
118 }
119 };
120 });
121 }());
122})();
Copied!

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.

1editor.ui.registry.addIcon(name, svgData);
Copied!

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.

1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <defs>
3 <symbol id="bubbles" viewBox="0 0 576 512">
4 ...
5 </symbol>
6 <symbol id="books" viewBox="0 0 576 512">
7 ...
8 </symbol>
9
10 </defs>
11</svg>
Copied!

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

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

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.

1(function () {
2 var helloworld = (function () {
3 'use strict';
4 
5 tinymce.PluginManager.add("helloworld", function (editor, url) {
6 
7 /*
8 Add a custom icon to TinyMCE
9 */
10 editor.ui.registry.addIcon('bubbles', '<svg class="icon"><use xlink:href="custom-icons.svg#bubbles4"></use></svg>');
11 
12 /*
13 Use to store the instance of the Dialog
14 */
15 var _dialog = false;
16 
17 /*
18 An array of options to appear in the "Type" select box.
19 */
20 var _typeOptions = [];
21 
22 /**
23 * Get the Dialog Configuration Object
24 *
25 * @returns {{buttons: *[], onSubmit: onSubmit, title: string, body: {}}}
26 * @private
27 */
28 function _getDialogConfig()
29 {
30 return {
31 title: 'Hello World Example Plugin',
32 body: {
33 type: 'panel',
34 items: [{
35 type: 'selectbox',
36 name: 'type',
37 label: 'Dropdown',
38 items: _typeOptions,
39 flex: true
40 }]
41 },
42 onSubmit: function (api) {
43 // insert markup
44 editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');
45 
46 // close the dialog
47 api.close();
48 },
49 buttons: [
50 {
51 text: 'Close',
52 type: 'cancel',
53 onclick: 'close'
54 },
55 {
56 text: 'Insert',
57 type: 'submit',
58 primary: true,
59 enabled: false
60 }
61 ]
62 };
63 }
64 
65 /**
66 * Plugin behaviour for when the Toolbar or Menu item is selected
67 *
68 * @private
69 */
70 function _onAction()
71 {
72 // Open a Dialog, and update the dialog instance var
73 _dialog = editor.windowManager.open(_getDialogConfig());
74 
75 // block the Dialog, and commence the data update
76 // Message is used for accessibility
77 _dialog.block('Loading...');
78 
79 // Do a server call to get the items for the select box
80 // We'll pretend using a setTimeout call
81 setTimeout(function () {
82 
83 // We're assuming this is what runs after the server call is performed
84 // We'd probably need to loop through a response from the server, and update
85 // the _typeOptions array with new values. We're just going to hard code
86 // those for now.
87 _typeOptions = [
88 {text: 'First Option', value: '1'},
89 {text: 'Second Option', value: '2'},
90 {text: 'Third Option', value: '3'}
91 ];
92 
93 // re-build the dialog
94 _dialog.redial(_getDialogConfig());
95 
96 // unblock the dialog
97 _dialog.unblock();
98 
99 }, 1000);
100 }
101 
102 // Define the Toolbar button
103 editor.ui.registry.addButton('helloworld', {
104 text: "Hello Button",
105 icon: 'bubbles',
106 onAction: _onAction
107 });
108 
109 // Define the Menu Item
110 editor.ui.registry.addMenuItem('helloworld', {
111 text: 'Hello Menu Item',
112 context: 'insert',
113 icon: 'bubbles',
114 onAction: _onAction
115 });
116 
117 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it
118 // This is optional.
119 return {
120 getMetadata: function () {
121 return {
122 name: "Hello World example",
123 url: "https://www.martyfriedel.com/blog/tinymce-5-creating-a-plugin-with-a-dialog-and-custom-icons"
124 };
125 }
126 };
127 });
128 }());
129})();
Copied!

You may be interested in...