Aloha Editor

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

The Block Plugin

After reading this guide, you will be able to:

We suggest that you open up the block demo located at src/demo/block/index.html of the Aloha Source, as it contains lots of usage examples and best practices.

1 What are Aloha Blocks?

Aloha Blocks (Blocks) are non-editable areas of a website that often have some properties that can be edited via the Aloha user interface.

Some use cases for blocks include:

  • Displaying a vCard of a person from an address book as a paragraph inside an editable, where the person whose information is to be displayed can be chosen through the Aloha UI
  • Displaying a custom “inline image” in continuous text, which could float either left or right, with an image caption that can be set using Aloha
  • Showing a list of news inside or outside an editable.
  • Creating a “column” container that can contain other blocks or other contents

Some properties of blocks:

  • Blocks can occur inside or outside of Aloha Editables
  • Both <span> and <div> elements can be blockified (i.e. converted to blocks)
  • Blocks can contain nested editable areas
  • Blocks can be copy/pasted and dragged/dropped if they are inside an Aloha editable
  • Blocks can be deleted using backspace or DEL if inside an editable

2 Enabling the Block Plugin

Aloha Blocks are implemented as a plugin called block, which is part of the common bundle.

Furthermore, the paste plugin is also a requirement in order to use blocks. Thus, just add common/block,common/paste to the data-aloha-plugins loading list.

As the contenthandler plugin currently cleans up the HTML very rigidly, this can interfere with blocks. If you use them together, make sure to test thoroughly that no unwanted HTML is removed.

2.1 Initializing Blocks

Blocks need to be initialized before they can be used. Most conveniently, it should be done when the page is loaded. The simplest way to initialize a block is by using the .alohaBlock() function on a jQuery collection. For example, to make .vcard a block, just use jQuery('.vcard').alohaBlock().

Make sure to wrap the initialization code inside an Aloha.ready() callback, to make sure Aloha is fully loaded by then.

You can use the reverse function .mahaloBlock() to “unblock” the elements in a jQuery collection.

It is only allowed to convert span or div elements into an Aloha Block. Otherwise, an error will be thrown. So watch the browser’s console output when debugging! The reason is that edit-icons can’t otherwise be added to the block root element. For example, edit-icons wouldn’t work correctly in an ‘a’ root element. The reason that edit-icons have to be added to the blockified element instead of being absolutely positioned is that absolutely positioned edit-icons may overlap with content.

The alohaBlock function takes a configuration object of Block Attributes, which are set on the block.

2.2 Block Attributes

Each block can have multiple block attributes, which are like configuration parameters and can influence the rendering of blocks. A block attribute key may only be lowercase, and can contain only a-z, 0-9, - and _ in the name. The block attribute value must be a string:


// Valid block attributes
{
	key: 'value',
	_foo: 'bar',
	'my-special-attr': 'Yeah',
	'attr-09': 'Test some very long string',
	another: '{"json": "encoded as string"}'
}
// Invalid block attributes
{
	kEy: 'value'
	foo: false,
	bar: {
		json: "foo"
	}
}

Because block attributes are stored as data attributes on the block DOM node, we must be quite restrictive concerning the allowed keys, and only allow string values.

Block attributes can be set at construction time through .alohaBlock(attrs) or using the block.attr() function at runtime.

All block attributes that start with aloha-block- are internal and can only be set during construction time.

2.3 Block Types

One special block attribute is called aloha-block-type, which must be set to one of the block types registered at the BlockManager. It can be only set during construction time, and if it is not set, the DefaultBlock is automatically chosen.

Depending on the block type, a different Block class will be instantiated. Later, you will be introduced to writing your own block type.

The stripped-down EmptyBlock type is available by default and is provided as a minimal block implementation with no additional behavior.

Aloha shows configuration errors on the Firebug or WebKit console; so watch this area for any errors, for example block types not being found.

2.4 Block Attribute Overriding Sources

When calling .alohaBlock on an element, the following data is merged together:

  • { aloha-block-type: 'DefaultBlock' }
  • The block attributes specified in the .alohaBlock(attr) function
  • All data- attributes on the corresponding DOM node.

That is, if a DOM node that should be blockified has a data-aloha-block-type property, this property is always used. Otherwise, the aloha-block-type property from the .alohaBlock() function is used (if given). If nothing is specified, the DefaultBlock is used.

The same is done for all block attributes, not only aloha-block-type.

2.5 Default Settings

Block construction such as the following is very common:


Aloha.ready(function() {
	Aloha.jQuery('.foo').alohaBlock({
		'aloha-block-type': 'MySpecialBlock'
	});
	Aloha.jQuery('.bar').alohaBlock({
		'aloha-block-type': 'DebugBlock'
	});
});

To make such initialization code easier to write and more declarative, this can also be written inside the Aloha settings:


Aloha.settings.plugins.block.defaults {
	'.foo': {
		'aloha-block-type': 'MySpecialBlock'
	},
	'.bar': {
		'aloha-block-type': 'DebugBlock'
	}
}

Using Aloha settings is the preferred way of initialization, as it is easier to read.

3 Interacting with Blocks

After a block has been initialized, it can be retrieved through the BlockManager.getBlock() method. This method accepts a variety of arguments:

  • the ID of the block (as in <span id="....">)
  • the DOM element of the block
  • the jQuery object of the block

Thus, the following is all possible and returns the same Block object instance:


require(['block/blockmanager'], function(BlockManager) {
	var b1 = BlockManager.getBlock('myBlock'); // ID
	var b2 = BlockManager.getBlock(jQuery('#myBlock')); // jQuery object
	var b3 = BlockManager.getBlock(jQuery('#myBlock').get(0)); // DOM object
});

After you retrieved have a block instance, you can use the public API of it. The most important methods are:

  • attr(key, value) to set key to value
  • attr({key1: value1, key2: value2}) to set multiple values simultaneously
  • attr(key) to retrieve the value for key
  • attr() to retrieve all key/values as object
  • activate() to activate the block
  • deactivate() to deactivate the block
  • unblock() to remove this block, but retain the DOM Element

When an attribute is changed through attr, the block is re-rendered automatically.

4 Writing a Custom Aloha Block

When writing a custom block, you should do so in your own aloha plugin. Inside the plugin module, you need to register the Aloha Blocks with the Block Manager. An example skeleton is as follows:


define([
	'aloha/plugin',
	'block/blockmanager',
	'blockdemo/block'
], function(Plugin, BlockManager, block) {
	"use strict";
	return Plugin.create('blockdemo', {
		init: function() {
			BlockManager.registerBlockType('MyCustomBlock', block.MyCustomBlock);
		}
	});
});

define([
	'block/block'
], function(block) {
	var MyCustomBlock = block.AbstractBlock.extend({
		// ... your custom code here ...
	});

	return {
		MyCustomBlock: MyCustomBlock
	};
});

Now, you can implement the main API of the block, as explained in the next section.

4.1 Initialization and Rendering API

The first method you can override is the init($element, postProcessFn) method. There, you get the jQuery $element as argument, and can use it to register custom event handlers or initialize the block contents, for example. The second parameter is a function that always needs to be executed after init() is complete. Furthermore, you can set block attributes using the attr() method if needed.

init() requires you to call postProcessFn, as this enables you to do asynchronous queries inside init().

init() can be called multiple times under some circumstances; so do not rely on the fact that init is only run once in your code. See the API doc about init() for further explanations on this.

After the init() method, the $element is augmented by additional DOM nodes, which are needed f.e. for the drag/drop handles of the block.

The second place you will most certainly override is the update($element, postProcessFn) method. This method is always called when one or multiple block attributes have changed, so you are able to run any code you want inside there, manipulating $element.

In some use cases, you will want to do some asynchronous work inside the update() method, like fetching an updated rendering of the element via AJAX from the server side. That is the reason of the postProcessFn callback function you get as second method argument: This function must always be called after the $element has been modified, as it renders the drag/drop handles, if necessary.

Because we add some special DOM nodes to the $element (for displaying the drag/drop handles for example), you should not rely on stuff like the number of child elements of $element. If you still need to do this, make sure to filter out all elements which have an aloha-block-handle CSS class applied (as they are internal elements).

4.2 Custom Block Handles

If you wish to write custom block handles, e.g. for deleting a block or adding new blocks, you need to override the renderBlockHandlesIfNeeded method. There, you can add DOM nodes to this.$element, and style them as handles using CSS.

There are two rules to follow:

  • First, the method must be idempotent, that is, it needs to have the same behavior no matter how often it is called. This means, for example, that if this method inserts a drag handle, it is only allowed to do so if the drag handle is not yet inserted.
  • Second, the method must mark all DOM nodes that are added with the CSS class aloha-block-handle such that they are marked as internal.

The default block handles function looks as follows, rendering a drag handle:


renderBlockHandlesIfNeeded: function() {
	if (this.isDraggable()) {
		if (this.$element.find('.aloha-block-draghandle').length == 0) {
			this.$element.prepend('<span class="aloha-block-handle aloha-block-draghandle"></span>');
		}
	}
}

if you use image elements as icons, mark them with the class ‘aloha-ui’, otherwise the image plugin will pick them up as normal content images that can be resized etc.

4.3 Nested Aloha Editables

If you want to mark a certain area inside a block as Aloha editable again, you just need to apply the aloha-editable CSS class to it. If the default behavior is not what you want, you can also call $element.find(...).aloha() in the init() and/or update() method.

5 Editing API

The attributes of an Aloha Block can be edited through an automatically generated User Interface in the Aloha Sidebar. Of course, this user interface needs to know which block attributes are editable. For that, an Aloha Block can contain a schema that defines this information. Simply override the getSchema() method and make it return a schema.

A basic schema can look like this:


getSchema: function() {
	return {
		symbol: {
			type: 'string',
			label: 'Stock Quote Name'
		}
	};
},		

It just defines that the block attribute symbol is of type string and has a certain label.

Additionally, the Aloha Block needs a title, which is shown in the sidebar. Just set the title property of your block, or for more advanced computations override the getTitle() method.

5.1 Introducing Editors

Every form element in the sidebar is represented internally through an editor class, which defines the behavior of the given form element.

You might now wonder how the system knows that an element of type string shall be edited through an input field. For that, the EditorManager is responsible. It contains a mapping from data types to editor classes, for example a mapping from the string data type to the StringEditor.

5.2 Available Editors

So far, the following data types/editors are available (each with an example):

5.2.1 string

{
	type: 'string',
	label: 'My Label'
}

Output: <input type="text" />

5.2.2 number

{
	type: 'number',
	label: 'My Label',
	range: {
		min: 0,
		max: 5,
		step: 0.5 // values 0, 0.5, ...,  4.5, 5
	}
}

Output: <input type="range" min="0" max="5" step="0.5" />

5.2.3 url

{
	type: 'url',
	label: 'My Label'
}

Output: <input type="url" />

5.2.4 email

{
	type: 'email',
	label: 'My Label'
}

Output: <input type="email" />

5.2.5 select

{
	type: 'select',
	label: 'Position',
	values: [{
		key: '',
		label: 'No Float'
	}, {
		key: 'left',
		label: 'Float left'
	}, {
		key: 'right',
		label: 'Float right'
	}]
}

Output: <select>...</select> (with the correctly active option pre-selected)

5.2.6 button

{
	type: 'button',
	buttonLabel: 'Click me!',
	callback: function() {
		// This function is executed when the button is clicked.
	}
}

Output: <button />

5.3 Writing a Custom Editor

For writing custom editors, just check the AbstractEditor and AbstractFormElementEditor inside lib/editor.js, as well as the default editor implementations. It should be quite self-explanatory :-)

In case you do not extend the AbstractFormElementEditor you just need to remember one thing — Make sure to throw a change event on the editor class, with the changed value as a parameter:


this.trigger('change', this.getValue());

Then the framework takes care of updating the attribute in the Aloha Block accordingly.

If you subclass AbstractFormElementEditor, you mostly do not need to deal with event handling yourself, as this is done for you. This can greatly simplify the editors.

Check the example editors in plugins/common/block/lib/editor.js, they are really easy and small.

6 Advanced Topics

Here, we will give an overview of some advanced integration tips and tricks.

6.1 Block Collections

Sometimes, you want to create blocks that are mainly a container for other blocks. An example is a “Column” block, which should accomodate other blocks. Now, there are two supported possibilities for that.

First, you can mark your columns with the CSS class aloha-editable, and then these columns can contain other blocks. Use this when you want to allow content to be placed between your blocks.

Second, you can mark your columns with the CSS class aloha-block-collection. Then, the Aloha Blocks inside become sortable: You see that they have a drag handle now. Furthermore, they can now be deleted using the standard backspace or delete keys.

Check the example blocks for a demo of this feature.

6.2 Custom Floating Menu

When the Aloha Blocks are active, we set a custom Floating Menu scope called Aloha.Block.(alohaBlockType}, so, for example, Aloha.Block.DefaultBlock. You add buttons to the floating menu if you want to show them when a specific block is active.

6.3 Disabling the sidebar editor

Sometimes, you want to embed Aloha into a bigger system, and you do not want to use the default Aloha sidebar for editing. Because of this, it is possible to disable the sidebar attribute editor as follows:


Aloha.settings.plugins.block.sidebarAttributeEditor = false;

Then, you need to listen to some events on the BlockManager, most notably the block-selection-change event, which is triggered each time the block selection changes.


BlockManager.bind('block-selection-change', function (blocks) {
	// blocks is an array now, where the first element is the selected block
	// and the other elements are the ancestor blocs.
	// If the array is empty, no block has been selected.
});

The block-selection-change is cumbersome to rely on when you don’t care about the global state of “which blocks are selected” but when you do care about the local state of each block, i.e. when you want to do things whenever a block gets or loses focus, use the block-activate and block-deactivate events.


BlockManager.bind('block-activate', function (blocks) { });
BlockManager.bind('block-deactivate', function (blocks) { });

6.4 Preventing switch of scope for a block

The block plugin will switch the current scope to be block-specific whenever a block is activated. To prevent it from doing so just add the data-block-skip-scope="true" attribute to the element you plan to turn into a block. As long as its value is “true” the scope will not be switched.

6.5 Enabling/Disabling drag & drop for blocks

If a block is placed inside an editable, it can be draged/dropped by default. If you want to prevent drag/drop behaviour for all blocks, add the following configuration option:


Aloha.settings.plugins.block.dragdrop = false;

Also, it is possible to allow users to toggle the drag/drop behaviour per editable by placing a button in the floating menu. To define the toggle drag/drop button for all editables, use the following configuration:


Aloha.settings.plugins.block.config.toggleDragdrop = true;

Alternatively, you can show this button only for selected editables:


Aloha.settings.plugins.block.editables = {
	'#editable-1': { 'toggleDragdrop': true },
	'#editable-2': { 'toggleDragdrop': false },
};

With the following settings the toggle button can be made to toggle the drag and drop feature for all editables instead of per-editable. Turning this setting on will override any of the per-editable settings above and will make the toggle button always visible.


Aloha.settings.plugins.block.config.toggleDragdropGlobal = true;

6.6 Defining Dropzones

When a block is being dragged, it can be dropped to any editable available in the page by default. To specify explicit drop targets for blocks inside an editable, use the `dropzones` option.


Aloha.settings.plugins.block.editables = {
	'#editable-1': { 'dropzones': [ '#editable-1', '#editable-2' ]},
	'#editable-2': { 'dropzones': [ '#editable-2' ]},
};

In the above example, blocks defined in “#editable-1” can be dragged to either “#editable-1” or “#editable-2”. However, blocks in “#editable-2” can only be dragged within itself.

You can also define the dropzones globally.


Aloha.settings.plugins.block.dropzones = [ '#editable-1', '#editable-2' ];

7 Internals

For this work, numerous IE hacks were needed. Especially in areas like Drag/Drop, Deletion and Copy/Paste with regards to IE7 and IE8, which differ considerably in their behavior. See the compatibility matrix below for the tests which have been run.

7.1 Browser Compatibility Matrix

Firefox 7 Chrome 17 IE7 IE8 IE9 Unit Test Written
General Aloha
General Blocks
Drag & Drop of inline elements
Drag & Drop of block-level elements
Copy & Paste (✓) works in Emul. Mode; IE7 always dies on second copy/paste
Cut & Paste (✓) works in Emul. Mode; IE7 always dies on second copy/paste
Deletion of single blocks (block-level)
Deletion of single blocks (inline)
Deletion of blocks being part of selection
nested inline Blocks inside editables
nested block-level Blocks inside editables
nested inline Blocks drag/drop
nested block-level Blocks drag/drop
block-collection: basic functionality
block-collection: delete block-level blocks
block-collection: drag/drop of block-level b
Caret handling of inline blocks