TinyMCE 5: Creating a custom Dialog Plugin (and with Custom Button Icons)
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:
Get started with a basic TinyMCE 5 Plugin
Make your Plugin have a Dialog using TinyMCE 5's UI components
Update the Dialog and its data dynamically after instantiation
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:
The API creates breaking changes – v4 to v5 is not a simple upgrade, and will (in most cases) require work
Adding custom icons is dramatically different, now referenced using SVGs rather than font icons
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:
The easy one – basic form elements of different field type
The harder one – how to dynamically update elements with data after instantiation
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 triggered10 editor.insertContent("<p>Content added from my Hello World plugin.</p>")11 }12 13 // Define the Toolbar button14 editor.ui.registry.addButton('helloworld', {15 text: "Hello Button",16 onAction: _onAction17 });18 19 // Define the Menu Item20 editor.ui.registry.addMenuItem('helloworld', {21 text: 'Hello Menu Item',22 context: 'insert',23 onAction: _onAction24 });25 26 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it27 // 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})();
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: true17 }]18 },19 onSubmit: function (api) {20 // insert markup21 editor.insertContent('<p>You selected Option ' + api.getData().type + '.</p>');22 23 // close the dialog24 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: false37 }38 ]39 });40}
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: _onAction101 });102 103 // Define the Menu Item104 editor.ui.registry.addMenuItem('helloworld', {105 text: 'Hello Menu Item',106 context: 'insert',107 onAction: _onAction108 });109 110 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it111 // 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})();
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);
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>
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>');
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 button103 editor.ui.registry.addButton('helloworld', {104 text: "Hello Button",105 icon: 'bubbles',106 onAction: _onAction107 });108 109 // Define the Menu Item110 editor.ui.registry.addMenuItem('helloworld', {111 text: 'Hello Menu Item',112 context: 'insert',113 icon: 'bubbles',114 onAction: _onAction115 });116 117 // Return details to be displayed in TinyMCE's "Help" plugin, if you use it118 // 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})();