July 09th, 2010

guide  |   development  |   ajax

CKEditor is mostly just perfect when you need a wysiwyg editor. Its image adding dialog is fine, too, as long as you don't do any image resizing. Resizing is only done with CSS and it is not possible to have a smaller thumbnail generated. A large image will be made smaller but the visitor's browser will still download the large version and it takes longer to download the image. Also the image's quality gets poorer if the browser is forced to recalculate image dimensions. I tackled the problem and succeeded in adding an Ajax-enabled thumbnail generation button to the editor.

This guide is meant to show how in general it is possible to implement your own actions for CKeditor . The details of image handling are not shown since that is another topic on its own right.

*ckdir* is used as a shorthand for the folder where the editor is located on the server.

The steps to implement the thumbnail generation are:
  1. The logic of the dialogbox that allows image URL to be inserted or selected from the server is located in the file *ckdir*/plugins/image/dialogs/image.js . Since the file is packed and encoded, we need to get the original source file. It is located in *ckdir*/_source/plugins/image/dialogs/image.js. Copy the contents from there, overwriting the packed file.

  2. On to adding the button: near approximately line 769 there is the following code:

    1. html : '<div>'+
    2.    '<a href="javascript:void(0)" tabindex="-1" title="' + editor.lang.image.unlockRatio +
    3.    '" class="cke_btn_locked" id="btnLockSizes" role="button"><span class="cke_label">' + editor.lang.image.unlockRatio + '</span></a>' +
    4.    '<a href="javascript:void(0)" tabindex="-1" title="' + editor.lang.image.resetSize +
    5.    '" class="cke_btn_reset" id="btnResetSize" role="button"><span class="cke_label">' + editor.lang.image.resetSize + '</span></a>'+
    6.    '</div>'


  3. This is the html code that generates buttons. We'll add our button and an Ajax spinner by replacing the last line with "div" in it with this:

    1. '<a href="javascript:void(0)" tabindex="-1" title="' + editor.lang.image.generateThumbnail +
    2. '" class="cke_btn_thumbnail" id="btnGenThumb" role="button"><span class="cke_label">' + editor.lang.image.generateThumbnail + '</span></a>'+
    3. '</div><div style="position:relative"><img alt="" id="activitySpinner" src="/images/spinner-small.gif" style="position:absolute;left:0px;top:0px;display:none;" /></div>'


  4. Some css style definitions must be added to the editor's active skin. If for example the skin called Kama is used, then the file is located in *ckdir*/skins/kama/dialog.css . Something similar may be added to the end of the file:

    1. .cke_skin_kama .cke_dialog a.cke_btn_thumbnail {
    2.     background-image:url("images/thumbnail.jpg");
    3.     background-repeat:no-repeat;
    4.     border:1px none;
    5.     float:left;
    6.     clear:both;
    7.     margin-left:5px;
    8.     font-size:1px;
    9.     height:14px;
    10.     width:9px;
    11. }


  5. We'll need to implement an action that will capture button clicks. In order to do this, image.js needs to opened again and near line 202 begins the definition of function imageDialog. A bit later in the file there is the definition of contents - array ( contents : [ ... )  and inside it, some more lines below is an onLoadhandler. The code in there is similar to this:

    1. type : 'html',
    2. style : 'margin-top:10px;width:40px;height:40px;',
    3. onLoad : function()
    4. {
    5.    // Activate Reset button
    6.    var  resetButton = CKEDITOR.document.getById( 'btnResetSize' ),
    7.   ratioButton = CKEDITOR.document.getById( 'btnLockSizes' );
    8.    if ( resetButton )
    9.    {
    10.   resetButton.on( 'click', function(evt)
    11.   {
    12.            resetSize( this );
    13.      evt.data.preventDefault();
    14.   }, this.getDialog() );
    15.   resetButton.on( 'mouseover', function()
    16.   {
    17.      this.addClass( 'cke_btn_over' );
    18.   }, resetButton );
    19.   resetButton.on( 'mouseout', function()
    20.   {
    21.      this.removeClass( 'cke_btn_over' );
    22.   }, resetButton );
    23.    }


    This is the place where we'll insert the code to handle the click on the thumbnail button.

  6. The code for handling the click is below:

    1. var resetButton = CKEDITOR.document.getById( 'btnResetSize' ),
    2.   ratioButton = CKEDITOR.document.getById( 'btnLockSizes' ),
    3.   thumbButton = CKEDITOR.document.getById( 'btnGenThumb' );
    4.                          
    5.   if( thumbButton )
    6.   {
    7.     thumbButton.on('click', function(evt){
    8.       var width = this.getValueOf( 'info', 'txtWidth' ),
    9.       height = this.getValueOf( 'info', 'txtHeight' ),
    10.       url = this.getValueOf( 'info', 'txtUrl' ),
    11.       filename = url.split('/')[url.split('/').length-1];
    12.     if(url.length > 0 && height.length && width.length)
    13.     {
    14.       var d = new Date();
    15.       var newFilename = width+'x'+height+'-'+d.getTime()+'-'+filename;
    16.       var newUrl = '/files/editor/.thumbs/'+newFilename;
    17.       var outerThis = this;
    18.       if (window.XMLHttpRequest)
    19.       {
    20.           xhr = new XMLHttpRequest();
    21.       }
    22.       else
    23.       {
    24.         if (window.ActiveXObject)
    25.         {
    26.             xhr = new ActiveXObject("Microsoft.XMLHTTP");
    27.         }
    28.       }
    29.       xhr.onreadystatechange = function()
    30.       {
    31.         if(xhr.readyState == 4)
    32.         {
    33.             document.getElementById('activitySpinner').style.display = 'none';
    34.           outerThis.setValueOf( 'info', 'txtUrl', newUrl);
    35.         }
    36.         else
    37.         {
    38.             document.getElementById('activitySpinner').style.display = '';
    39.         }
    40.       };                                 
    41.       xhr.open('POST', '/admin/editor/MakeThumb', true);
    42.       xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");                
    43.       xhr.send('width='+width+
    44.       '&height='+height+
    45.       '&origName='+url+
    46.       '&filename='+newFilename);                               
    47.  
    48.     }
    49.   }, this.getDialog() );


First we'll get a reference to the thumb button (line 3) so that later on this reference can be used to determine whether the button was clicked. If the button reference was retreived, then we'll add click handler for it (line 7).

For making a thumbnail, we'll need to have it's width and height, the url for the original file and name of the file (to generate a unique name). These are obtained on lines 8-11.

Checking if the height, width and url are filled in (line 12).

A new file name is generated from measures and date, adding some randomness, too (lines 14-16).

Ajax request is made in lines 18-46 and consists of a cross browser part to get the proper XMLHttpRequest or ActiveXObject, depending on whether the user has older Internet Explorer or Opera/Firefox/Safari (lines 18-28), and executing code.

On lines 29-40 the logic of state change is defined. During the request, an Ajax spinner image has to be shown and if the request will be successful, the spinner is hidden and the thumbnail URL is set as the new URL of the image.

On lines 41-46 the Ajax request is started. /admin/editor/MakeThumb should be replaced with the proper URL pointing to the script that generates the thumbnail.

N.B.! It is not advisable to use CKeditor's own Ajax handling because it only supports GET requests. Passing the original image URL through GET, even by urlencoding it, is not very stabile across URL handling mechanisms of different CMS-ses or frameworks.