Aloha Editor

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

The Block Plugin

After reading this guide, you will be able to:

  • Understand what Aloha Blocks are
  • use already-given Aloha Blocks
  • write your own blocks and editors
  • Integrate Blocks into your CMS or backend system

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, which often have some properties being editable through 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 can be chosen through the Aloha UI
  • Display a custom “inline image” in continuous text, which could float either left or right, and where the image caption is again changeable using Aloha
  • Show a listing of news inside or outside an editable.
  • Create a “column” container which 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)
  • can contain nested editable areas
  • can be copy/pasted and dragged/dropped if they are inside an Aloha editable
  • 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, you need to load the paste plugin. 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 most simple way to initialize a block is by using the .alohaBlock() function on a jQuery collection, i.e. 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.

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 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 is only allowed to be lowercase, and shall 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 which 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 is being instanciated. Later, you will be introduced in writing your own block type.

Aloha shows configuration errors on the firebug or webkit console; so watch this place for any errors, f.e. because block types were not 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 which should be blockified has a data-aloha-block-type property, this one is always used. Else, 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 more easy to write and more declarative, this can be also 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 more easy 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 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 objecz
  • activate() to activate the block
  • deactivate() to deactivate the block

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 at the Block Manager. An example skeleton 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 f.e. custom event handlers or initialize the block contents. The second parameter is a function which needs to be run always 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 augumented 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 called always when one or multiple block attributes 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 be always 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 f.e. 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 which 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>');
		}
	}
}

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 which 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 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 the 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 want to show some advanced integration tips and tricks.

6.1 Block Collections

Sometimes, you want to create blocks which 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, so 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.
});

7 Internals

For this work, numerous IE hacks were needed. Especially in areas like Drag/Drop, Deletion and Copy/Paste, especially IE7 and IE8 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