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.
Plugin options - options.js
and plugininfo.php
There are often times that you will want to pass options, or values stored in Moodle's PHP API, to the JavaScript API in your plugin. TinyMCE has a detailed API to support parsing and validation of per-instance options which make this relatively easy.
To help make this easier, several helpers exist.
On the PHP side of the API, the plugininfo.php
file can implement an optional plugin_with_configuration
interface and define a get_plugin_configuration_for_context
function:
<?php
namespace tiny_example;
use context;
use editor_tiny\plugin;
use editor_tiny\plugin_with_configuration;
class plugininfo extends plugin implements plugin_with_configuration {
public static function get_plugin_configuration_for_context(
context $context,
array $options,
array $fpoptions,
?\editor_tiny\editor $editor = null
): array {
return [
// Your values go here.
// These will be mapped to a namespaced EditorOption in Tiny.
'myFirstProperty' => 'TODO Calculate your values here',
];
}
}
The values passed in may come from location including as site configuration values (that is get_config()
), values based on capabilities, or values based on the $options
passed in.
On the JavaScript side of the API, an options.js
file is used to handle parsing, validation, and fetching of the values.
A complete example of the options.js implementation
import {getPluginOptionName} from 'editor_tiny/options';
import {pluginName} from './common';
// Helper variables for the option names.
const myFirstPropertyName = getPluginOptionName(pluginName, 'myFirstProperty');
/**
* Options registration function.
*
* @param {tinyMCE} editor
*/
export const register = (editor) => {
const registerOption = editor.options.register;
// For each option, register it with the editor.
// Valid type are defined in https://www.tiny.cloud/docs/tinymce/6/apis/tinymce.editoroptions/
registerOption(myFirstPropertyName, {
processor: 'number',
});
};
/**
* Fetch the myFirstProperty value for this editor instance.
*
* @param {tinyMCE} editor The editor instance to fetch the value for
* @returns {object} The value of the myFirstProperty option
*/
export const getMyFirstProperty = (editor) => editor.options.get(myFirstPropertyName);
After being passed from the PHP API, the Moodle integration names properties according to the plugin from which they originate. In order to fetch the value out, you must use the getPluginOptionName()
function to translate this value.
const myFirstPropertyName = getPluginOptionName(pluginName, 'myFirstProperty');
Before being set, or fetched back out, each property must be registered with the TinyMCE API. This is done for each instance of the editor:
export const register = (editor) => {
const registerOption = editor.options.register;
// For each option, register it with the editor.
// Valid type are defined in https://www.tiny.cloud/docs/tinymce/6/apis/tinymce.editoroptions/
registerOption(myFirstPropertyName, {
processor: 'string',
});
};
See the tinymce.EditorOptions API documentation for further details on the register
method.
Finally, it is recommended that you then create helpers to fetch the values back out.
export const getMyFirstProperty = (editor) => editor.options.get(myFirstPropertyName);
Editor options are specific to an instance of the editor, therefore the editor
must be passed as an argument.
You may have multiple helpers, for example you may have a helper to process the value and return a boolean state of the value.
export const hasMyFirstProperty = (editor) => editor.options.isSet(myFirstPropertyName);
Editor configuration - configuration.js
The TinyMCE Editor only allows for very minimal configuration by administrators. This is a deliberate decision made to reduce the complexity of the User Interface, and to encourage consistency.
To support this, for a plugin to place commands (that is buttons, and menu items) in the User Interface, they must modify the TinyMCE configuration.
The TinyMCE Menus and Toolbar configuration describes the expected format of this configuration.
In order to make your configuration changes you must create a Configuration
object, which contains a configure()
function. This value should be resolved at the end of your plugin.js
file, for example:
import * as Configuration from './configuration';
export default new Promise(async(resolve) => {
// ... Plugin code goes here.
resolve([pluginName, Configuration]);
})
The Moodle TinyMCE Integration will call Configuration.configure
after the initial editor configuration has been put in place.
The configure
function is passed the current configuration and must return an object which is merged into the configuration.
For example, to add an item to the 'content' toolbar region, you would define your configure
function return an Object containing the toolbar
key:
export const configure = (instanceConfig) => {
return {
toolbar: addToolbarButtons(instanceConfig.toolbar, 'content', buttonName),
};
};
This is used to replace any matching keys in the existing instanceConfig
.
The above example makes use of a helper from the editor_tiny/utils
module, which contains a number of useful helpers.
A complete example of configuring a menu and toolbar for tiny_example
import {
startdemoButtonName,
startdemoMenuItemName,
} from './common';
import {
addMenubarItem,
addToolbarButtons,
} from 'editor_tiny/utils';
const getToolbarConfiguration = (instanceConfig) => {
let toolbar = instanceConfig.toolbar;
toolbar = addToolbarButtons(toolbar, 'content', [
startdemoButtonName,
]);
return toolbar;
};
const getMenuConfiguration = (instanceConfig) => {
let menu = instanceConfig.menu;
menu = addMenubarItem(menu, 'file', [
startdemoMenuItemName,
].join(' '));
return menu;
};
export const configure = (instanceConfig) => {
return {
toolbar: getToolbarConfiguration(instanceConfig),
menu: getMenuConfiguration(instanceConfig),
};
};