TinyMCE Editor Plugins
Moodle includes the TinyMCE text editor as standard from Moodle 4.1, and it can be installed from the plugins database for Moodle versions 3.11, and 4.0.
The editor_tiny
editor supports the inclusion of subplugins, which have the namespace tiny_[pluginname]
.
File structure
TinyMCE subplugins are located in the /lib/editor/tiny/plugins
directory. A plugin should not include any custom files outside of its own plugin folder.
Each plugin is in a separate subdirectory and consists of a number of mandatory files and any other files the developer is going to use.
Some of the important files are described below. See the common plugin files documentation for details of other files which may be useful in your plugin.
The directory layout for the tiny_example
plugin.
lib/editor/tiny/plugins/example
├── amd
│ ├── build
│ │ ├── commands.min.js
│ │ ├── commands.min.js.map
│ │ ├── common.min.js
│ │ ├── common.min.js.map
│ │ ├── configuration.min.js
│ │ ├── configuration.min.js.map
│ │ ├── options.min.js
│ │ ├── options.min.js.map
│ │ ├── plugin.min.js
│ │ └── plugin.min.js.map
│ └── src
│ ├── commands.js
│ ├── common.js
│ ├── configuration.js
│ ├── options.js
│ └── plugin.js
├── classes
│ ├── plugininfo.php
│ └── privacy
│ └── provider.php
├── lang
│ └── en
│ └── tiny_example.php
├── settings.php
└── version.php
You will notice that the JavaScript is broken down into a number of source files.
This separation is optional, but fits a convention demonstrate in the TinyMCE codebase, and make the code easier to read and understand.
Creating a new plugin
We highly recommend using the Plugin Skeleton Generator when creating a new plugin.
For the sake of simplicity, this documentation assumes that you have created a new Plugin using the following skeleton configuration:
component: tiny_example
name: Example Plugin
release: "0.1.0"
copyright: 2022 Andrew Lyons <andrew@nicols.co.uk>
features:
settings: true
privacy:
haspersonaldata: false
uselegacypolyfill: false
tiny_features:
buttons:
- name: startdemo
category: content
text: Start demo
menuitems:
- name: startdemo
category: file
text: 'Start the demo'
options:
- name: myFirstProperty
type: string
Generating the plugin skeleton
Once you have created a plugin skeleton configuration, you can generate your plugin using the cli/generate.php
command:
php admin/tool/pluginskel/cli/generate.php tiny_example.yml
This will generate a working skeleton file for your plugin. Remember that you component name must start with tiny_
.
The plugin skeleton only produces source files for JavaScript. You will need to run grunt
to compile this code.
We highly recommend using grunt watch
during development to simplify your workflow.
cd lib/editor/tiny/plugins/example && npx grunt amd && cd -
Key files
There are a number of key files within the generated plugin skeleton, described below.
common.js
The common.js file is used to store a set of variables used by other parts of the plugin.
Its usage is optional, but recommended as it reduces code duplication, and the potential for typos and mistakes. It also makes it easier to refactor code later.
An example common.js file generated by the plugin skeleton generator
This example includes:
- the plugin name (
tiny_example/plugin
); - an icon, whose name is
tiny_example
; - a button for the start demo action, whose name is
tiny_example_startdemo
; and - a menu item for the start demo action, whose name is
tiny_example_startdemo
.
const component = 'tiny_example';
export default {
component,
pluginName: `${component}/plugin`,
icon: component,
startdemoButtonName: `${component}_startdemo`,
startdemoMenuItemName: `${component}_startdemo`,
};
Typically this file will be included in other JS files in the plugin, usually only fetching the required variables, for example:
import {component, pluginName} from './common';
plugin.js
The plugin.js is the entrypoint to the plugin code. It is primarily responsible for registering the plugin with the TinyMCE API, and the Moodle Integration of the Editor.
An example plugin.js file generated by the plugin skeleton generator
import {getTinyMCE} from 'editor_tiny/loader';
import {getPluginMetadata} from 'editor_tiny/utils';
import {component, pluginName} from './common';
import {register as registerOptions} from './options';
import {getSetup as getCommandSetup} from './commands';
import * as Configuration from './configuration';
// Setup the tiny_example Plugin.
export default new Promise(async(resolve) => {
// Note: The PluginManager.add function does not support asynchronous configuration.
// Perform any asynchronous configuration here, and then call the PluginManager.add function.
const [
tinyMCE,
pluginMetadata,
setupCommands,
] = await Promise.all([
getTinyMCE(),
getPluginMetadata(component, pluginName),
getCommandSetup(),
]);
// Reminder: Any asynchronous code must be run before this point.
tinyMCE.PluginManager.add(pluginName, (editor) => {
// Register any options that your plugin has
registerOptions(editor);
// Setup any commands such as buttons, menu items, and so on.
setupCommands(editor);
// Return the pluginMetadata object. This is used by TinyMCE to display a help link for your plugin.
return pluginMetadata;
});
resolve([pluginName, Configuration]);
});
The plugin can be broadly broken down into several different areas:
The default export
Every plugin must return a default export containing a new Promise.
This allows the API to load multiple plugins in parallel with minimal blocking.
// Imports go here.
export default new Promise(async(resolve) => {
// Configure the plugin here.
// Resolve when the plugin has been configured.
resolve([pluginName, Configuration]);
});
Preparation
The TinyMCE API does not support asynchronous code in the plugin registration. Therefore any asynchronous tasks must be complete before registering the plugin with the TinyMCE API, and before resolving the Promise.
In the following example, we fetch the tinyMCE API, a set of plugin metadata to use, and a command setup function to call later on.
// Note: The PluginManager.add function does not support asynchronous configuration.
// Perform any asynchronous configuration here, and then call the PluginManager.add function.
const [
tinyMCE,
pluginMetadata,
setupCommands,
] = await Promise.all([
getTinyMCE(),
getPluginMetadata(component, pluginName),
getCommandSetup(),
]);
Registration of the plugin
Once all of the dependencies are available, we can register the plugin with the TinyMCE PluginManager API.
In this example, we register a plugin whose name is represented as a string in the pluginName
variable.
Whenever a new editor instance is created, it will call the callback providing the editor
argument.
At the end of the plugin instantiation, it returns a pluginMetadata
object, which contains information about the plugin displayed in the help dialogue for the plugin.
// Reminder: Any asynchronous code must be run before this point.
tinyMCE.PluginManager.add(pluginName, (editor) => {
// Register any options that your plugin has
registerOptions(editor);
// Setup any commands such as buttons, menu items, and so on.
setupCommands(editor);
// Return the pluginMetadata object. This is used by TinyMCE to display a help link for your plugin.
return pluginMetadata;
});
In this example, the plugin describes a set of options which will be passed from the PHP description of the plugin - these are handled by the registerOptions(editor)
call.
It also has a set of 'commands', which are a generic term used to describe any Buttons, MenuItems, and related UI features of the editor.
commands.js
TinyMCE supports a range of commands. These are further defined in the TinyMCE API: tinymce.editor.ui.Registry.
Most plugins will make use of one or more of the following commands, but others are also available:
Plugins may use any parts of the TinyMCE API that they need.
An example commands.js file generated by the plugin skeleton generator
import {getButtonImage} from 'editor_tiny/utils';
import {get_string as getString} from 'core/str';
import {
component,
startdemoButtonName,
startdemoMenuItemName,
icon,
} from './common';
/**
* Handle the action for your plugin.
* @param {TinyMCE.editor} editor The tinyMCE editor instance.
*/
const handleAction = (editor) => {
// TODO Handle the action.
window.console.log(editor);
};
export const getSetup = async() => {
const [
startdemoButtonNameTitle,
startdemoMenuItemNameTitle,
buttonImage,
] = await Promise.all([
getString('button_startdemo', component),
getString('menuitem_startdemo', component),
getButtonImage('icon', component),
]);
return (editor) => {
// Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button.
editor.ui.registry.addIcon(icon, buttonImage.html);
// Register the startdemo Toolbar Button.
editor.ui.registry.addButton(startdemoButtonName, {
icon,
tooltip: startdemoButtonNameTitle,
onAction: () => handleAction(editor),
});
// Add the startdemo Menu Item.
// This allows it to be added to a standard menu, or a context menu.
editor.ui.registry.addMenuItem(startdemoMenuItemName, {
icon,
text: startdemoMenuItemNameTitle,
onAction: () => handleAction(editor),
});
};
};
The TinyMCE PluginManager.add
function requires all code to be called synchronously - that is to say that all Promises must be resolved before it is called.
See more information on the Editor instance in the tinymce.Editor API documentation.
handleAction(editor)
The handleAction function is an example of one way in which the various buttons and menu items can handle their activation.
The action passes a reference to the instance of the TinyMCE editor in the editor
variable.
It should be possible to interact with all required parts fo the TinyMCE API using this value.
getSetup()
getSetup()
function in the example above is an asynchronous function which returns a synchronous function.
This is important because the TinyMCE PluginManager API requires all code to be synchronous and already exist.
In this example strings are fetched fro the button and menu titles, and the icon is fetched using a Mustache Template. All of these functions return a Promise and therefore we must wait for them to resolve before returning the function which uses them.
The curried setup function
The getSetup()
function returns a new function which is called from plugin.js
during the instantiation of each editor instance. If you have five editors on a page, then this function is called five times - once per editor instance.
This function is passed a partially-configured tinymce.Editor instance on which it can call the registry commands to define the various buttons.