Aloha Editor

Aloha Editor Guides

These guides help you to make your content editable and to develop Aloha Editor.

Writing Plugins

After reading this guide, you will be able to:

  • Create plugins to adjust Aloha Editor to your needs.
  • Load and define dependencies with RequireJS.
  • Include other resources from your plugins

1 What’s a plugin?

A plugin extends Aloha Editor with new behavior and can be loaded by the user asynchronously. It should be self-contained, so that it can be used for other Aloha installations.

A full-blown plugin has the following file structure:

  • pluginname
    • css – Stylesheets
    • doc – Documentation, if exists
    • img – Image assets
    • lib – RequireJS modules
    • nls – Localization files, see “Localization and I18N”: #Localization
    • res – other resources which do not fit into other directories, like JavaScript templates
    • test – Unit tests
    • vendor – Legacy JavaScript dependencies, not managed by RequireJS
    • package.json – Metadata about your package, in CommonJS format

All file names are, by convention, lowercase.

2 A basic Aloha plugin

For a very basic plugin, nothing else than a plugin module in lib/ has to be created. The module is named as pluginname-plugin.js.

/custom/helloworld/lib/helloworld-plugin.js

define(['aloha/plugin'], function (Plugin) {
    'use strict';

    return Plugin.create('helloworld', {
        init: function () {
            // Executed on plugin initialization
        }
    });
});

If your plugin should be published to the public you should consider writing a package.json file with metadata about the plugin. Furthermore, the name of your plugin name should be globally unique.

This code requires the core plugin module (which defines the plugin class) and creates a plugin with the name pluginA.

pluginA/lib/pluginA-plugin.js

define(['aloha/plugin', 'aloha/console'], function (Plugin, console) {
    'use strict';

    return Plugin.create('pluginA', {
    	defaults: {
    		value: 10
    	},
        init: function () {
            // Executed on plugin initialization
			console.log(this.settings.value);
        }
    });
});

As developer you can define plugin default settings by specifying Aloha.defaults.pluginA and these default setting will be merged with the Aloha.setttings. Once defined as default you can always access the settings without extensive checking its existence.

To read the settings of a specific plugin you can use


	var settings = Aloha.require('pluginA/pluginA-plugin').settings;

3 Ephemera

Somtimes a plugin must inject non-content into the editable. For example, a plugin may add a class to a link to for visual highlighting. Or elements are injected to make the content more interactive.

For example, the table plugin alters the table structure in order to display row and column handles that can be clicked to select a row or column.

In Aloha Editor we call this “ephemeral content”.

After this ephemeral conten was injected into the editable, it must be removed again before the actual content of the editable can be retrieved.

There are two mechanisms currently in place.

3.1 makeClean

The first mechanism is the Plugin.makeClean method. This method can be overridden and implemented by a plugin to provide custom cleaning of the editable. The Plugin.makeClean method will receive the editable element as a jQuery object.

The makeClean mechanism is not as fast and flexible as the second approach, and should therefore be considered deprecated.

3.2 aloha/ephemera

The second mechanism is the aloha/ephemera module.

In the aloha/ephemera module a global registry is maintained where plugins register their ephemeral classes or attributes.


define(['aloha/plugin', 'aloha/ephemera'], function (Plugin, Ephemera) {
    'use strict';

    Ephemera.classes('aloha-my-plugin-custom-class');
    Ephemera.attributes('SOME-ELEMENT.some-attribute');

    ...
});

The above works for well known classes and attributes that are considered ephemeral at a global scope.

Individual elements and attributes can be “marked”


define(['aloha/plugin', 'aloha/ephemera'], function (Plugin, Ephemera) {
    'use strict';

    // my-element and descendants elements are all marked as ephemeral.
    Ephemera.markElement($('#my-element')[0]);

    // some-attribute of my-element-2 is marked as ephemeral.
    Ephemera.markAttribute($('#my-element-2')[0], 'some-attribute');
  
    // my-wrapper itself is marked as ephemeral, but descendant elements are not.
    Ephemera.markWrapper($('#my-wrapper')[0]);

    ...
});

When it is time to get the contents of an editable by calling editable.getContents() the ephemeral content will be automatically pruned by a call to Ephemera.prune(), which removes all ephemera from a copy of the editable’s content before the copy is returned.

4 Localization

If you have some labels in your plugin, these need to be made available in multiple languages, as Aloha is capable showing its user interface in a wide variety of languages.

Localization is done using a slightly adjusted version of the i18n module of RequireJS

4.1 Folder Structure for Labels

Localized strings are stored in the folder nls (Natural Language Support) of your plugin. The file/directory structure inside nls is as follows:

  • nls/
    • i18n.js – contains the English strings, used as fallback.
    • de/
      • i18n.js – contains the German strings
    • fr/
      • i18n.js – contains the French strings
    • … continue for the other languages …

The English i18n file has the following structure:

myplugin/nls/i18n.js

define({
	"root": {
		"your.key.here": "Label in English",
		"your.other.key.here": "Some more Text",
		// continue here for all labels
	},
	"de": true,
	"fr": true
});

Inside root, all the keys and labels are placed. Furthermore, when a German language file is available, the key de needs to be set to true, and this is done for all languages available.

A localized language file has a simpler structure:

myplugin/nls/de/i18n.js

define({
	"your.key.here": "Beschreibung in Deutsch",
	"your.other.key.here": "Noch mehr Text",
	// continue here for all labels
});

4.2 Using localized strings

Now, if we want to use the “your.key.here” key inside our application, we can use RequireJS to inject the correct language version for us:

myplugin/lib/something.js

define(['i18n!myplugin/nls/i18n'], function (i18n) {
	// your code...
	var localizedString = i18n.t('your.key.here');
	// your code...
});

By defining i18n!myplugin/nls/i18n as requirement, we get a special object injected which contains all localized strings. There exists a t('key') method on this object which returns the translation for the passed key.

5 Including CSS files

When your Aloha plugin needs some CSS files, you should place them in the css folder of your plugin. Your plugin can load the CSS file as follows:

myplugin/lib/myplugin-plugin.js

define(['aloha/plugin', 'css!myplugin/css/myplugin.css'], function (Plugin) {
    'use strict';
	
	// Add this stage css is loaded
	
    return Plugin.create('myplugin', {
        init: function () {
            // Executed on plugin initialization
        }
    });
});

The key part is to set a dependency on css!myplugin/css/myplugin.css. Then, the loader knows that it needs to load the myplugin.css stylesheet into the current website.

The css! requirejs plugin is just a convenience if you only need to load one or two additional plugins that are not included in the Aloha distribution Aloha. If you have many css files, they should be concatenated and minified to avoid performing many independent HTTP requests during loading, which significantly impacts load performance. When writing an Aloha common plugin, the css file must added to the main aloha css include src/css/aloha.css.

By default, CSS file loading is asynchronous, meaning that CSS files are loaded in the background, independently from the JavaScript loading. If you for some reason depend on the fact that a CSS file is included at a particular point in time (calles synchronous loading), you need to append the parameter !nowait to the dependency, so the whole dependency looks like css!myplugin/css/myplugin.css!nowait

6 Lifecycle of Plugins

Currently there are 2 different stages of Aloha plugins

  • aloha-plugins-loaded means that all initially defined plugins and their dependencies are loaded.
  • aloha-plugins-ready means that all plugins init method was called.

7 Dependencies between plugins

If a plugin A needs plugin B to work, you should not specify it as a dependency of plugin A in the define() methode, as this will break without a meaningful error message to the user.

Let’s see how to define a dependency to the block-Plugin:

pluginA/lib/pluginA-plugin.js

define(['aloha/plugin'], function (Plugin) {
    'use strict';

    return Plugin.create('pluginA', {
    	dependencies: ['pluginB'],
        init: function () {
        	
            // Executed on pluginA initialization
        	// pluginB ist not available here 
        
        	Aloha.require('pluginA/module', function (Module) {
        		// Use pluginB's module here
        	});
        }
    });
});

Here, we specify a dependency to another plugin using the dependencies property of our Plugin. Then, the plugin manager makes sure that the dependent plugin is actually loaded, and outpus an error message if not. When everything is loaded, we can access a module from the other plugin by using the requireAloha() statement, as shown for the pluginB in the example above.

8 PluginContentHandler

Plugins can register their own ContentHandler to customize the behavior of the GenericContentHandler for certain Html-Elements.

To register a PluginContentHandler, the Plugin must implement a “getPluginContentHandler” method. The method must return a map which maps a CSS-selector to a PluginContentHandler-function. The PluginContentHandler-function will be called every time the GenericContentHandler encounters an HTML-Element which matches the given CSS-selector. The function will get the current element, options and editable (similiar to a “regular” ContentHandler) and can then perform DOM-manipulation on the current element. A PluginContentHandler-function should only change the the element itself and not its children or parents.

A working implementation of the PluginContentHandler can be found in the List-plugin.

pluginA/lib/pluginA-plugin.js

define(['aloha/plugin'], function (Plugin) {
    'use strict';

    return Plugin.create('pluginA', {
        init: function () {},
        getPluginContentHandler: function () {
            return {
                'plugin-css-selector': function ($elem, options, editable) {
                    // do cleanup / manipulation of the $elem
                    ...
                    // return false to prevent the execution of other PluginContentHandler 
                    // and the GenericContentHandler on this element.
                    return false;
                },
                'another-plugin-css-selector': function ($elem, options, editable) {
                    // do cleanup / manipulation of the $elem
                    ...
                    // return true (or undefined) to allow the execution of other PluginContentHandler 
                    // and the GenericContentHandler on this element.
                    return true;
                }
            }
        }
    });
});