I recently created a module for ContentBox that allows you to embed “fiddles” from the excellent http://jsfiddle.net. If you’ve not used this site before, you really should :)

In developing this module, I had very specific goals, particularly for the user interface in the rich HTML content editor, namely CKEditor. My goals were simple:

  • Have toolbar icon for easy insert
  • Allow easy insert from context menu (right-click) within the content editor area
  • After content/config/whatever is inserted into content editor area, show something meaningful (other than ugly red iFrame box)
  • Allow user to act on inserted content–particularly, be able to edit all the properties of the content that were configured pre-insert

The first two are really easy. If you look at the source of the PasteBin module, you’ll see really quickly how to do those, so I’m not going to dwell on that. In this post, however, I do want to show how simple it is to make inserted content regions richly editable, treating them like complex data, rather than just simply strings.

NOTE: In the following, I include code-snippets, but often omit unimportant or redundant bits. Be sure to grab the entire source to see everything in its full context :)

Insert Content Region

Before we look at creating the editable content region, let’s see what data we want to manage:

  • height
  • width
  • src
  • tabs
    • js
    • css
    • html
    • resources
    • result

So a really easy way to manage this content within the editor itself is if we insert a tag. Each bit of data we want to manage can be an attribute of the tag, and we can then use the CKEditor API to easily access and manipulate those attributes’ values. But we’re getting ahead of ourselves. Let’s insert the tag first!

In the module’s views/home/entryHelper.cfm file, let’s build up our tag. It can be any tag, so for the example, let’s make a fake tag called “fiddle“:

function prepareFiddle( inputs ) {
   var vals = [],
       iframe = '';
   inputs.each(function() {
      if( this.type=='checkbox' ) {
         vals.push( this.checked ? true : false );
      }
      else {
         // add values to array
         vals.push( this.value );  
      }
   })
   if( vals[0]=='' ) {
      alert( 'Please enter a URL!' );
      return false;
   }
   // create double-mustache syntax
   iframe += '<fiddle height="{2}" width="{3}" src="{0}" result="{4}" js="{5}" resources="{6}" css="{7}" html="{8}" >jsFiddle - {1}</fiddle>'.format( vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], vals[7], vals[8] );
   // insert into editor
   sendEditorText ( iframe );
}
function sendEditorText(text){
   $("###rc.editorName#").ckeditorGet().insertElement( CKEDITOR.dom.element.createFromHtml( text ) );
   closeRemoteModal();
}

Most of this is unimportant. Basically, prepareFiddle() loops over the fields in the main insertion form (found in views/home/entry.cfm), creates an array of values, and then builds up a string which is the fiddle tag, along with each attribute and its value. Finally, it uses sendEditorText() to insert the string into the CKEditor.

Make that Content Region Editable!

Now that we’ve inserted a data-rich content region (read: “tag”), we need to make it editable. To do this, we’ll need to open our module’s plugin.js and do some voodoo. In a nutshell, what we want to happen is that when we place our cursor inside our inserted content region and right-click, we want to see a new option in the context menu (e.g., ‘Edit Fiddle). And when we click that new option, we want to open up a modal dialog that will have editable fields that map to each data point that we inserted with our new tag. Ready? Let’s go!

In our plugin, we should have a structure like this:

(function(){
   CKEDITOR.plugins.add('cbjsFiddle',{
      init: function( editor ) {
         ...
      }
   });
})();

All that we’ll add will be in the init() method.

First things first. Let’s add the item to the context menu. This is easy:

// Register a new context menu item for editing existing fiddle.
editor.addMenuItem( 'jsFiddleItem', {
   // Item label.
   label : 'Edit Fiddle',
   // Item icon path using the variable defined above.
   icon: this.path + 'jsfiddle.png',
   // Reference to the plugin command name.
   command : 'fiddleDialog',
   // Context menu group that this entry belongs to.
   group : 'clipboard'
});

This is all pretty self-explanatory. Notice the “command”, though. This is the command that should be executed when the menu item is acted upon. So to handle this, we need to register a new command with the editor, like so:

// Define an editor command that allows modification of the fiddle. 
editor.addCommand( 'fiddleDialog', new CKEDITOR.dialogCommand( 'fiddleDialog' ) );

More straightforwardness. Mr. Editor, please be aware that when the fiddleDialog command is executed, you should create an instance of a dialog that I will define as “fiddleDialog”.  Thanks.

Ok, easy enough. So far, we’ve set up a new context menu item, and registered its action with the editor. Next, we need to tell CKEditor how the heck it’s supposed to know that it should show the dialog when we right-click on a fiddle element, but not anything else. We can do that by adding a listener to the context menu that is bounded to a specific element selector:

// listener for right-click on <fiddle> element (and only <fiddle> element)
editor.contextMenu.addListener( function( element ) {
   // Get to the closest <fiddle> element that contains the selection.
   if ( element )
      element = element.getAscendant( 'fiddle', true );
      // Return a context menu object in an enabled, but not active state.
      if ( element && !element.isReadOnly() && !element.data( 'cke-realelement' ) )
         return { jsFiddleItem : CKEDITOR.TRISTATE_ON };
        // Return nothing if the conditions are not met.
   return null;
});

You can read the comments, but the most important part is in bold. The listener will only fire the context menu with the new context menu item IF the context of the click is a fiddle element. Nice.

Now we need to create the dialog box that will contain some fields and allow us to edit the properties of our fiddle tag.

// create new dialog for editing fiddle
CKEDITOR.dialog.add( 'fiddleDialog', function ( editor ){
   return {
      // Basic properties of the dialog window: title, minimum size.
      title : 'Edit jsFiddle',
      minWidth : 400,
      minHeight : 200,
      // Dialog window contents.
      contents : [
         {
    	    // Definition of the Basic Settings dialog window tab (page) with its id, label and contents.
            id : 'tab1',
            label : 'Basic Settings',
            elements : [
    	       {
    	          // Dialog window UI element: a text input field.
                  type : 'text',
                  id : 'url',
                  // Text that labels the field.    								
                  label : 'URL',
                  // Validation checking whether the field is not empty.
                  validate : CKEDITOR.dialog.validate.notEmpty( "URL field cannot be empty" ),
                  // Function to be run when the setupContent method of the parent dialog window is called.
                  // It can be used to initialize the value of the field.
                  setup : function( element ) {
                     this.setValue( element.getAttribute( 'src' ) );
                  },
                  // Function to be run when the commitContent method of the parent dialog window is called.
                  // Set the element's text content to the value of this field.
                  commit : function( element ) {
                     element.setAttribute( 'src', this.getValue() );
                  }
               },
               ......
            ]
         }   
      ],
      // This method is invoked once a dialog window is loaded. 
      onShow : function() {
         // Get the element selected in the editor.
         var sel = editor.getSelection(),
         // Assigning the element in which the selection starts to a variable.
         element = sel.getStartElement();			
         // Get the <fiddle> element closest to the selection.
    	 if ( element )
    	    element = element.getAscendant( 'fiddle', true );
    	 // Create a new <fiddle> element if it does not exist.
    	 // For a new <fiddle> element set the insertMode flag to true.
    	 if ( !element || element.getName() != 'fiddle' || element.data( 'cke-realelement' ) ) {
            element = editor.document.createElement( 'fiddle' );
            this.insertMode = true;
         }
    	 // If an <fiddle> element already exists, set the insertMode flag to false.
    	 else
    	    this.insertMode = false;
            // Store the reference to the <fiddle> element in a variable.
            this.element = element;
            // Invoke the setup functions of the element.
    	    this.setupContent( this.element );
      },				
      // This method is invoked once a user closes the dialog window, accepting the changes.
      onOk : function() {
         // A dialog window object.
    	 var dialog = this,
    	     fiddle = this.element;
    	 // If we are not editing an existing fiddle element, insert a new one.
    	 if ( this.insertMode )
    	    editor.insertElement( fiddle );
    	    // Populate the element with values entered by the user (invoke commit functions).
    	    this.commitContent( fiddle );
      }
   };
});

There’s a lot of code here, but it’s pretty straightforward. In the contents config, we simply create an array of fields definitions. Each field section has two important methods:  setup() and commit(). Each is self-explanatory. Finally, we close it out with the onShow() and onOk() methods which more or less handle what is done when the modal window is opened and closed, respectively.

Putting this altogether, we now have a fully working editable content region…Woohoo!!

Styling the Content Region

It would be nice if our custom content region “looked” like something. To do this, we can add in a custom stylesheet to the CKEditor styles.

First, we’ll create out stylesheet in our plugin’s folder. I’m calling mine editor.css. In editor.css, I’ve added the following styles:

fiddle {
    border: dotted 2px #333;
    color: #fafafa;
    border-radius: 4px;
    padding:10px;
    background-color:#3D6E99;
    display:block;
    font-weight:bold;
}

To integrate this into the CKEditor styles, we can add the following in plugin.js‘s init() section:

CKEDITOR.config.contentsCss = typeof CKEDITOR.config.contentsCss=='object' ? CKEDITOR.config.contentsCss.push( this.path + 'editor.css' ) : [CKEDITOR.config.contentsCss, this.path + 'editor.css'];

Rendering the Data Correctly

Ok, so we have everything working, we need to do some content massaging in our module’s interceptor. After all, our final rendered content needs to be an iFrame…which obviously we don’t have right now :)

Easily enough done, however. In the interceptor, we simply need to do the following:

  /**
   * Intercepts on cb_onContentRendering to replace custom tag syntax
   */
  function cb_onContentRendering( required any event, required struct interceptData ) {
    // regex for fiddle tag syntax
    var regex 	= "<fiddle\b[^>]*>(.*?)</fiddle>";
    // get string builder
    var builder = arguments.interceptData.builder;
    // find regex matches 
    var targets = reMatch( regex, builder.toString() );
    // loop over all matches
    for( var match in targets ) {
       .... // replace tag attributes with real ones for iframe tag...
       // find the match syntax position
       var pos = builder.indexOf( match );
       // get the length
       var len = len( match );
       // Replace it
       builder.replace( javaCast( "int", pos ), JavaCast( "int", pos+len ), replacer )			
    }
  }

I’ve omitted a bunch of code (feel free to review the full version at GitHub), but basically I have a simple regex that finds all “fiddle” tags in my page or post’s content, loops over all the matches, and replaces each match with the replacement string that I build within the loop.

Here’s the result:

Wrapping Up

As we’ve seen, with just a bit of extra code, we can easily create rich, editable content regions within the CKEditor that allows the customization of our module’s custom content to live beyond the initial insert. It’s not the ideal solution for every addition to the content editor, but for certain situations I think it’s a nice option. Enjoy!