Notes while deploying TinyMCE v5
Posted Under: Internet,JavaScript,Rich text editors
Introduction
These are my notes as I adopted TinyMCE v5 for use as the editor for writing things to be published on the Internet. Like this post, for example.
My overall verdict is that it’s excellent, and I’m writing this after having added quite some unusual features to it. In particular, syntax highlighting where the both the segment and language are chosen manually. The API offered for writing custom plugins plays along with you. The examples in the docs are usually more or less copy-paste for the actual need.
I can’t say I felt very comfortable by the commercial attempt to make the impression that using tinyMCE requires a paid-for license to get a decent editor. The contrary is true. The free package covers functionality that is perfectly fine for comfortable editing. So if there’s anyone out there considering to adopt this editor for a business web venture, I warmly suggest getting down to the details, and in particular to understand the LGPL. Maybe the paid-for plans suit some scenarios, but keep in mind that Tiny’s website is quite misleading in this respect. Unfortunately, they’re really not alone.
Getting started
Download the self-hosted Dev Package from here (the Community package doesn’t have the non-minified JS file).
Create the following HTML file where the tinymce.min.js file is, inspired by this page:
<!DOCTYPE html>
<html>
<head>
<script src="tinymce.min.js" referrerpolicy="origin"></script>
<script type="text/javascript">
tinymce.init({
selector: '#mytextarea'
});
</script>
</head>
<body>
<h1>TinyMCE Quick Start Guide</h1>
<form method="post">
<textarea id="mytextarea">Hello, World!</textarea>
</form>
</body>
</html>
and load that file.
Plugins
Official plugins are loaded from js/plugins/{name}/plugin.js (or plugin.min.js, depending on whether tinemce.js or tinymce.min.js was used, even though this can be changed). The {name} part is the name that appears in the call to tinymce.init(), in the “plugins” property.
The plugin files themselves contain a function enclosure that is executed when the plugin is loaded. That’s it. For example, this plugin.js just makes an alert window while the editor is being loaded. Useless, but makes the point:
(function () {
'use strict';
alert("Hello!");
}());
Apparently, TinyMCE just executes the plugin.js’ content, which in this case is a function enclosure that executes itself.
For this to work, the snippet above needs to be named plugin.js, and be in a directory whose name is listed in the “plugins” configuration parameter.
To get a really minimal example of a plugin, I suggest looking at hr/plugin.js. With the understanding that it just gives a function that is executed, it’s quite simple to figure out what it does.
It’s quite odd that TinyMCE’s way to load plugins is one file for each plugin — that is, one HTTP request for each plugin used. As there can easily be a dozen of these, it slows down the page’s load, and also increases the chance for a failure. I haven’t found an official solution for loading all at once, even though it’s quite easy to achieve. For example, concatenate all plugin files into a single file:
$ cat $(find plugins/ -name plugin.js) > all.js
And then make sure that the concatenated file is loaded along with the web page with a line like this in the <head> section:
<script src="/js/all.js"></script>
(or actually, virtually anywhere in the page). A smaller file can be obtained by using the .min.js files. And of course, this JavaScript file may also contain other things.
Note that the plugin’s name still needs to be listed in the ”plugins” configuration parameter. Using the concatenated script file just eliminates the attempt to load a dedicated plugin.js file from a URL that is based upon the plugin’s name. What counts is that the plugin name has been registered with tinymce.PluginManager.add(). If it has, the tinyMCE doesn’t bother trying to load it separately.
If loading the concatenated file fails, or it lacks plugins that are listed in the “plugins” configuration parameters, errors of this sort appear in the web console:
GET https://thesite.com/js/plugins/link/plugin.js net::ERR_ABORTED 404 (Not Found) Failed to load plugin: link from url plugins/link/plugin.js
It’s worth mentioning that tinyMCE doesn’t initialize immediately on the call to tinymce.init(), so the script that registers the plugin can be executed after this call. Which is quite unexpected, but in the positive direction.
Creating icons (with GIMP)
Basically, draw anything in a square image. Then create a path (possibly by using Select > To Path) and then in the Path tab, right-click the path and choose Export Path to create an SVG file.
The SVG then needs editing: Remove everything in the <svg> tag except for width and viewBox. The latter doesn’t exist in MCE’s default icons, but it’s necessary to scale the coordinates to the target size. Change the width and height to 24.
In the <path> tag, remove everything except for the “d” assignment. If there are internal areas in the image (there usually are), it’s probably easier with setting “fill-rule” attribute set to “evenodd”. This is because the any internal loop becomes a hole.
Otherwise, and in particular if there are overlapping areas, the default “nonzero” the correct choice. Note however that “nonzero” doesn’t necessarily mean a union of overlapping paths. This is only the case when all paths are drawn in the same orientation (e.g. all are drawn clockwise). The idea of “nonzero” is that a hole can be made by drawing a counter-clockwise path inside an area that is enclosed by a path that was drawn clockwise (and vice versa).
These two options are explained on this page.
The evenodd is added inside the <svg> attribute, so it terminates with something like
[ ... ] fill-rule="evenodd"/></svg>'
Then turn it all into a single line with this Perl one-liner:
$ perl -e 'local $/; $a=<>; $a =~ s/[ \n\r\t]+/ /g; $a =~ s/> </></g; print $a;' < myicon.svg
Then, register the icon with something like:
editor.ui.registry.addIcon('highlight', '<svg width="24" height="24" viewBox="0 0 256 256"><path d=" [ ... ] " /></svg>');
The coordinates of viewBox may vary, of course, depending on the original image size.
Adding items to the main menu
Apparently, there’s no dynamic way to add items to the main menu. It’s a bit understandable, since doing that would require to define where, within each drop-down of the menu, the item should go.
So the way to do it, is to define it on the init call’s configuration, typically by adding another header to the menu. So the configuration reads like this:
tinymce.init({
[ ... ]
plugins: 'myplugin [ ... ]',
menubar: 'file edit view insert format tools table mine help',
menu: { mine: { title: "Eli's extras", items: 'myitem' } },
[ ... ]
And then the registration for both toolbar and menu goes (the addButton() call is here for reference, and isn’t relevant):
editor.ui.registry.addButton('myitem', {
icon: 'myicon',
tooltip: 'Do this and that',
onAction: doit,
});
editor.ui.registry.addMenuItem('myitem', {
icon: 'myicon',
text: 'Do this and that',
onAction: doit,
});
It’s also possible to define one of the built-in menu headers, but that’s not forward-compatible, because it requires listing all items that appear under that header. So now imagine upgrading the editor, expecting to find new features, and not finding them because that specific menu header is overridden by some JavaScript snippet you wrote years ago, buried somewhere.
General notes
Some random jots.
- In the example code, the selector property is set to ‘#mytextarea’, which results in tinyMCE creating an iframe instead of the DOM element identified by the said ID. The main advantage of this is that the CSS of the edited area is separated from the surrounding page. To just edit any DOM element, add an “inline: true” property.
- Either way, the edited area doesn’t have to be a <textarea>, nor does it have to be enclosed in a <form> if the data submission to the server is done by other means than a plain form submit (e.g. with AJAX).
- The “body_class” property of the init object allows adding classes to the document enclosure. Using the same main class in the editor and the published document gives a true WYSIWYG effect.
- Setting the editor’s width property is somewhat tricky: The specified width includes the border and the scrollbar if such exists. Hence it’s wiser to set it wider by 17 pixels (or 20 pixels to be on the safe side) and add a max-width CSS property to the editor window’s body tag. In the absence of a scroll bar, there will be little unused space to the right. This might become apparent when something is wide and centered, so it reaches both side’s edges. And only in the absence of a scroll bar, of course, which doesn’t happen much when editing for real.
- The API for plugins is excellent. For operations directly on the DOM, use the DOMUtils API. It has rather advanced features like split().
- Formatting: There’s also a useful API for generating custom formats.
- There’s also an execCommand set of API calls, with something like
editor.execCommand('mceInsertContent', false, '<hr />');
The “false” in the command above, like in many other command usage examples shown on that page, goes to a function parameter named “ui”, apparently meaning that the call isn’t made by the user directly.
- Don’t enable the “quickbars” plugin, unless you want annoying popups whenever you select anything, or just press ENTER for that matter.
- The code that produces the menu and the toolbar, and its submenus, belongs to the theme, e.g. js/themes/silver/theme.js.
- The editor isn’t really initialized when tinymce.init() returns. Among others, this means that attempting to obtain a handle to it with e.g. tinymce.get() will fail immediately after that. My workaround was to register a function for init_instance_callback.
- getContent() returns the HTML with line breaks between block elements, which is a blessing (unlike innerHTML which is everything in one line).
- Toggle buttons are quirky, maybe even buggy: The “active” property was ignored flat (or did I mess up?), and this is worked around with an “onSetup” callback using setActive(). Even more important, if the toggle button doesn’t change anything in the editing area, the focus will remain on the button itself, which makes it appear to be in the active state even when it’s not. This is solved with a simple editor.onfocus(false) in the onAction handler, so the focus is moved away from the button itself.
- TinyMCE’s “change” event isn’t suitable for indicating to user that the document needs saving, since certain edits don’t fire it off. Some use the isDirty flag in function that is called recurrently, but I went for adding a mutation observer:
function mutation_callback(mutationsList, observer) { document.getElementById("savestatus").innerHTML = ''; } const observer_config = { attributes: true, characterData: true, childList: true, subtree: true, }; let observer = new MutationObserver(mutation_callback); observer.observe(editor.getBody(), observer_config);
Surprisingly enough, mutation_callback gets called back once after each edit operation, and isn’t bombarded as one might fear. This is because there’s a list of mutations that is passed over on each call (which is ignored in this case), so it’s one call for a lot of possible changes.
- The DOM tree may get obscured due to formatting. For example, selecting a region within a text area and toggling bold on and off results in three consecutive text areas (in Google Chrome), that aren’t optimized into one. This is pretty harmless as it doesn’t have any visual effect. Opening the Source Code window and clicking “Save” fixes this, as the DOM is rebuilt from HTML. And of course, when the document is reloaded from HTML. So this is a temporary and invisible quirk.
And a couple of CSS related jots:
- When there’s a <span> inside a <span>, class formatting of the inner span overrides that of the external one, regardless of anything. In particular, it may also override a CSS assignment made with !important. All formatting that isn’t declared explicitly by the span element itself is considered inherited format, and there’s no specificity calculation nor any other conflict resolution. All the relevant resolution rules apply only when several formats are imposed by the same DOM element.
- When the exact same selector is used in a CSS file (same level of specificity), the one appearing later overrides the previous. Citing this site: “When multiple declarations have equal specificity, the last declaration found in the CSS is applied to the element”.
Reader Comments
Hi, Eli.
We have somethings common, thats how I reached your articles written very thorouhly aand professionally.I’m saying that as someone reads by hundreds when researching..
Anyway, what we have in common? Thats quill and tinymce..
I needed a speech recognition (dictation) software for writing my own articles which I publish on my fathers web site which is a VPS, and a WordPress site..
Also, for our note-taking during live events.. So much to write..
After a thorough search, I found one, needed to customize somethings, and the way Quill worked, there were some problems like when pasting an image, the document would scroll all the way up and I would loose the cursor etc.
Also, what I wanted to do was to get a real WYSIWYG:
Because I both dislike TinyMce Classic and the new Gutenberg.. My solution was to export a previously written article to a static html.. Then make a clean up so that the sidebar + Featured İmage remains.. And in that article space create an “editable” region using the sites own css etc..
Poking into the code (the one with TextToSpeech + Quill)
I could manage to do what I needed..
Yet, as I mentioned earlier, due to some probs I experienced with quill.. I decided to look into another WYSIWYG rich text editor and just like you, I came to good old TinyMce..
There’s one problem; to incorporate TinyMce to the TTS program.. And there’s so little info I could find on the internet..
I am not an expert but I did write some code in the past and poking into this particular JS, I can see its orchestrating Quill.js like this: (I dont know if theres a limit; so I’ll continue with the following comment..)
This is the js related to Quill ..
function h() {
if (x() && “undefined” != typeof Quill) {
(le = new Quill(“#editor”, {
theme: “snow”,
modules: {
toolbar: “#toolbar”,
imagePaste: {},
htmlEditButton: {
syntax: true,
},
},
})).on(“text-change”, function (delta, oldDelta, source) {
de.dictation = y();
/* function y() {
return le ? le.root.innerHTML : “”;
} */
if (“api” === source) {
setTimeout(function () {
if (delta && delta.ops) {
for (let i = 0; i quill.setSelection(finalcaret, 0), 1); // this works
setTimeout(function () {
/* function g(match) {
ee = match ? match.index + match.length : le.getLength();
} */
g(le.getSelection());
}, 10);
}
}
});
le.on(“selection-change”, function (M) {
g(M);
});
if (de.dictation) {
le.pasteHTML(0, de.dictation);
}
if (!ue) {
P();
}
}
}
/**
* @param {!Object} match
* @return {undefined}
*/
function g(match) {
ee = match ? match.index + match.length : le.getLength();
}
/**
* @param {string} value
* @param {boolean} i
* @return {undefined}
*/
function m(value, i) {
if (le) {
if (i) {
le.pasteHTML(ee, value.replace(/^\s|\s$/g, “ ”));
} else {
le.insertText(ee, value);
}
}
}
/**
* @param {string} e
* @param {boolean} value
* @return {undefined}
*/
function v(e, value) {
te = value || false;
m(e, “&” === e.trim().charAt(0));
}
/**
* @return {?}
*/
function y() {
return le ? le.root.innerHTML : “”;
}
/**
* @return {?}
*/
function k() {
return le ? le.getText() : “”;
}
/**
* @param {string} i
* @return {undefined}
*/
function b(i) {
if (te) {
i = i.charAt(0).toUpperCase() + i.substr(1);
/** @type {boolean} */
te = false;
}
m(” ” + i);
}
I dont know the corresponding functions in TinyMce..
Eg the very first one.. .on(text-change) is it .isdirty I read that its the user but is it? What about when the source is API, meaning here, STT function.. etc etc..
I listed the needed terms as:
Source (user / API / Silent)
on(“text-change”)
delta (insert)
setSelection / getselection
on(“selection-change”)
pasteHTML()
inserttext()
root.innerHTML
.getText()
These are the ones needed to be translated for TinyMCE..
Would you be very kind to help on these?
Meanwhile there’s one doc I am trying to reach to look at as an example..
It’s JIRA’s Desktop Rich text editor.. It is basically TinyMCE and there’s a dropdown option called Jira Service Desk native Speech Recognition;
see link: https://community.atlassian.com/t5/Jira-Service-Management/Jira-Service-Desk-native-Speech-Recognition-error-quot-Sorry-I/qaq-p/779086
Today I downloaded JIRA and trying to reach the code to see as an example for TTS..
After this, if this is solved and TinyMCE it is..
I’ll be back to ask about this two articles about TinyMCE.
Thank you very much for you time.
Hi. Correction: I realize I wrote “TTS” several times above.. What I need is DICTATION which is denoted by STT (Speech to Text) or known as Speech Recognition..
Sorry about that typo..