• custom formatblock options

last modified November 20, 2010 by ejucovy


See http://trac.xinha.org/ticket/1541 -- this article is written as if the patch on that ticket has already been committed.  It is not valid for the current Xinha trunk.


Have you ever wanted to add custom options to the "formatblock" dropdown list?  On this site we have a "sidebar" option which wraps the block in a <div class="pullquote"> (and our CSS makes it move to the right-hand side of the page).  Here's how to do it.

There are three moving parts to know about:

  1. Putting something in the formatblock dropdown menu itself.
  2. Detecting which formatblock option the cursor is currently in.  For example, if the cursor is in a "heading", the formatblock menu shows "heading" instead of "normal".  If we're going to add a new option, Xinha needs to know how to detect if the cursor is already inside our option.
  3. Telling Xinha what to do when the option is selected -- how to create the HTML for our option. 


Putting something in the formatblock dropdown menu itself.

First, configure the formatblock block with some custom elements that aren't real HTML tags:
  xinha_config.formatblock = {
      'Normal': 'nosidebar',
      'Heading': 'h2',
      'Subheading': 'h3',
      'Pre-formatted': 'pre',
      'Sidebar': 'sidebar'

That was easy enough.  Now we have a new option, "sidebar", in our formatblock dropdown.


Detecting which formatblock option the cursor is currently in. 

By default, the detection algorithm looks for the tag we specified in "formatblock" -- starting at the cursor position, it looks for the nearest ancestor that matches any of the tags we've given.  Obviously "sidebar" isn't a valid HTML tag, so we need to tell Xinha how to know if we're inside a "sidebar".

To do this, we can use the `formatblockDetector` option.  We just need to provide a function that returns `true` if (and only if) the cursor is within a "sidebar" context.  (You don't need to worry about detecting if it's in another context first -- Xinha will handle that based on the order in which it calls the detection functions.)  The function should take two arguments: the xinha editor object, and the DOM element that the cursor is currently inside. 

Here's a simple function that walks up the DOM tree looking for a <div class="pullquote">:

  xinha_config.formatblockDetector['sidebar'] = function(xinha, el) {
      while (el !== null) {
          if (el.nodeType == 1 && el.tagName.toUpperCase() == 'DIV') {
              return /\bpullquote\b/.test(el.className);
          el = el.parentNode;
      return false



Telling Xinha what to do when the option is selected

Finally, we need to do something when the user selects the "sidebar" option.  For this, we can use an event hook.  When the option is selected, Xinha will fire an event `onExecCommand("formatblock", false, "<sidebar>")` so we need to provide an onExecCommand event listener that only acts if its first argument is "formatblock" and its third argument is "<sidebar>".  If those conditions are met, our listener should do the work it needs to do, and then return true to stop Xinha from doing anything else.  (If we don't return true, Xinha will try to call the browser's native execCommand("formatblock") function with an argument of "<sidebar>", and who knows what'll happen then.  We need to prevent that.)

var makeASideBar = function(xinha) {

el = xinha.getParentElement(); // get the DOM element that the cursor is currently inside

// now create a <div class="pullquote"> and put it around that element: 

el_parent = el.parentNode;

div = xinha._doc.createElement('div');

div.className = "pullquote";

el_parent.replaceChild(div, el);



xinha_config.Events.onExecCommand = function(cmdID, UI, param) {

if( cmdID != 'formatblock' ) { return false; } if( param != '<sidebar>' ) { return false; } if( param == '<sidebar>' ) { makeASideBar(this); // `this` is the xinha editor object return true; }


 OK!  That wasn't too hard.


Oh wait, there's more...

When you start to use this new option, you'll see there are some details we've omitted.  

First of all, depending on the HTML you're building, we might need to create an explicit command for removing the markup when the "normal" option is pressed.  (That's why I added a second formatblock option "nosidebar" in this example, though I didn't define it.)  You would just go through the same mechanisms to do that.

Second of all, your function that creates the HTML (`makeASideBar` in my example) will probably end up being more complicated.  You'll want to handle a variety of cases -- doing something special if some text is selected, or if you're inside certain tags; you might want to walk up the tree to find a block-level element to act on, rather than just the nearest element; and so on.

Figuring out the best way to handle it for your specific needs is up to you.