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
tovalue
- 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 | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |