2009/12/11

Adjusting the context menu of CKEditor

The easyUpload plugin aims to replace the default image & link plugins as they can be seen as too complex, so we can expect that the config won't include their buttons in the toolbar, but the context menu entries will still be shown even if the command isn't available in the toolbar.

So you might think that it's just a matter of removing the "image" plugin, but you'll find that the context menu is still there. After a little digging you might be able that this is due because the "forms" plugin has stated that "image" is a requirement (for the input type="image). Then the next step would be to remove the forms plugin, but maybe that's a little too much.

Or what about the link plugin? It does include the unlink command as well as the anchor, so if we remove that plugin we need to copy all that code to another file.

The solution would be to remove just the context menu entries for those items that we don't want. In order to do this you need to study a little how the context menu plugin works and you'll find that it stores the available items in an object: editor._.menuItems and when the context menu is fired, every listener returns the object that should be shown at that time, but if that entry doesn't in the editor._.menuItems then it just skips that order.

So we can do this in our plugin to remove the context menu for the image plugin:

    afterInit: function(editor)
    {
        delete editor._.menuItems.image;
    },

On the other side, this plugin creates its own entries for images and links, but maybe you want to use only the new image dialog, so we need to apply some cleanup to the context menu.

Then instead of just hardcoding the elements to remove from the context menu the approach is to review the elements that are defined for the toolbar comparing them with a list of items that can be removed if they aren't used and then finally remove from the context menu all such items.

    afterInit: function(editor)
    {
        // Remove the default context menu for elements that aren't being used in the toolbar.
        // This object is composed of the button name and the name of the context menu entry
        var removableEntries = {Image:'image',Link:'link',Unlink:'unlink',EasyImage:'easyimage',EasyLink:'easylink',EasyUnlink:'easyunlink'},
            // Get the data that is being used for the toolbar, we end with an array of arrays.
            toolbar =
                    ( editor.config.toolbar instanceof Array ) ?
                        editor.config.toolbar
                    :
                        editor.config[ 'toolbar_' + editor.config.toolbar ];

        // Loop the main array (composed of groups of commands)
        for (var i=0; i<toolbar.length; i++)
        {
            var items = toolbar[i];
            if (!items)
                continue;
           
            for (var j=0; j<items.length; j++)
            {
                var buttonName = items[j];
                // If it was marked at our check object remove it because it's in use
                if (removableEntries[buttonName])
                    delete removableEntries[buttonName] ;
            }
        }
       
        // Remove all the entries that aren't used in the toolbar
        for (var command in removableEntries)
            delete editor._.menuItems[ removableEntries[command] ];
    },

With this code our plugin will take care of leaving only the correct entries in the context menu without any extra configuration

2009/12/09

Plugin localization in CKEditor (vs FCKeditor)

If you work in the web you should have already realized that there are tons of people from every where and that not all of them speak English, and even if they are able to understand it they might to work with their native tonge, so providing the ability to translate your plugins it's important if you want them to be used anywhere.

Adding the plugin

In FCKeditor the registration call of the plugin did specify the available languages:

FCKConfig.Plugins.Add( 'easyupload', 'de,en') ;

In CKEditor there's no call to add a plugin, just a list of plugins and two other lists to add extra plugins or remove existing ones (each list is just a comma separated string with the names of the plugins). So that call would be something along the lines:

config.extraPlugins = 'easyupload';

But that doesn't specify the languages available for the plugin, now instead is the plugin itself the one that specifies what are its available languages:

CKEDITOR.plugins.add( 'easyupload',
{
    // translations
    lang : ['en'],
...

The translation file

The available translations must be placed as previously under the pluginfolder/lang/languagecode.js being pluginfolder the name of your plugin and languagecode the code of the language ('en' in the above case), so we would end with a file under plugins/easyupload/lang/en.js for example.

The structure of this file is quite different from the previous one. In FCKeditor the file was a simple list of properties added to the FCKLang object:

FCKLang['EuImgDialogTitle']  = 'Insert / Edit Image' ;

Now the file is made of one (or several depending on how you have defined your data) call to the CKEDITOR.plugins.setLang method that gets as parameters the name of the plugin, the language code and an object that contains the definitions of the strings:

CKEDITOR.plugins.setLang( 'easyupload', 'en',
    {
        easyimage :
        {
            toolbar: 'Insert/Edit an image',
...
        }
    }
)

Usage

And so the way to use it now is also slightly different, the "lang" object of the current editor instance has been augmented with your definitions:

        editor.ui.addButton( 'EasyImage',
            {
                label : editor.lang.easyimage.toolbar,
                command : 'easyimage',
                icon : this.path + 'images/image.gif'
            } );

Dialogs

Now all the dialogs are created from javascript objects, there are no html parts to load so there is no need for the "fcklang" attribute and the call to FCKLanguageManager.TranslatePage(document)

2009/12/08

Class selector in easyUpload

Following with this serie of posts about the new easyUpload plugin for CKEditor, today I'm gonna talk about the class selector for images.

What's the class selector

The selector is used due to two reasons:

  1. Avoid showing the user all the options about Border, HSpace, VSpace. This avoids clutter in the dialog, avoids the use of presentational attributes (changing the underlaying code to use sytles instead of attributes is not enough, they are placed there on each image and so they can't be used in a somewhat unified way) and puts the presentation in the stylesheet instead of the html document.
  2. Asking users to use a text box and learn the available class names that they can use it's not an option. They won't. It's not their job to learn the names that the designer has selected so they are gonna use any kind of cheatsheet, so it's better to offer that cheatsheet inline so they pick the one that they want and see how it does affect the image.

You can try to tell them to just upload the picture, don't change anything in the dialog about the presentation and use the Styles combo in the main menu, but this is too complex: If they shouldn't use something in the dialog, then they shouldn't see that option at all, and it seems logical to then put the options of the Styles combo related to the image in that dialog.

There's something about this option that you might argue that it shouldn't be done this way and it's the fact that only class names are used from the Styles data. It won't use the css style="", or any other attribute that you can specify in the definition of that Style, but I've already said that from my point of view this is better to avoid leaving scattered attributes and styles all around the HTML instead of putting the presentation in the stylesheet.

This is an example of a valid definition that would be shown in the class combo of the easyUpload dialog as "Nice picture" and if selected the image would end up with a class="highlighted"

    {
        name : 'Nice picture',
        element : 'img',
        attributes : { 'class' : 'highlighted'}
    },

CKEditor vs CKEditor

The UI of the dialog is quite similar to the previous one for FCKeditor (some small layout differences aside), but the underlying code for this feature is quite different.

In order to make this feature work, previously I just re-scanned the data in the FCK.Styles.GetStyles() array looking for element=img and the use of any class. But the new implementation of the styles combo makes this approach impossible. It's using private data locked inside the object so it can't be used outside of it, and thus the plugin now needs to copy the loading data code (fortunatelly the "resource manager" in CKEditor means that the file is loaded only once and then it's shared for every later request). Trivia: This investigation of the source code led to a bug found while trying to understand the behavior; The styles combo is "dead" until you first click on it, its initialization is delayed until it's shown so if you don't click on it, the data that it's shown is always the same because it has no data to match the current style of the selection.

The bad part about having to request the load of the stylescombo data is that if in the future the format of the data is augmented to allow XML files like it was possible in FCKeditor, or even if a plugin is added that does allow to parse the stylesheet and create the proper entries automatically, then that code won't work for this plugin due to the fact that it's hardcoded to load just the js definitions file, the easyUpload plugin would need to copy again the part about how to load the styles data.

The idea about creating a plugin that does scan the stylesheet and automatically populates the Styles data instead of having to define in an external file the available Styles is a clear advantage. I've been using one such plugin for several years in FCKeditor and I've never had to care about adjusting the (at that time) XML file with the definition of the style for each site. Just defining properly the css selectors in the stylesheet is enough to make the plugin parse the data and fill the combo. Unfortunately the code was developed for a company so it can't be shared, but I can assure you that it's quite simple; Due to the new structure (with private data) of CKEditor I'm not so confident about the ease of coding it to use this kind of feature in the new version. But if you look at the posts of beginners (and maybe not-so begginers) about the use of the Styles combo a question repeated over and over again is why the styles combo isn't filled with the data from their stylesheet.

2009/12/07

Using your own Uploader in CKEditor

CKEditor still doesn't include its own file manager, but the hooks to integrate your own uploader script or file manager are already in place, as you can check win CKFinder or any number of third party scripts that people have been writing.

Upgrading from FCKeditor

Maybe you want to use a previous uploader that you've used in FCKeditor and you are not sure about what are the differences, and the current documentation isn't clear enough for you. I'm pointing here the differences for the upload (or "quick upload") part, I might try to cover the slight differences in the "File browser" in other post.

  1. The file is sent as "upload" instead of "NewFile". Note that this is the value that you'll get using the default dialogs, but the name of the input is the id applied to the uiElement used to create the widget so in some situations you might get a different name.
  2. Some additional info is sent along the request as GET parameters: the name of the CKEditor instance, the current language code and a parameter to specify the callback function.
  3. The callback function is dynamic, you must read the "CKEditorFuncNum" parameter and send it back.
  4. There are no special error numbers in the callback function. Your script can return a url to use and a message to show to the user, both are optional so you can return just an URL that will be used in the dialog, or you can return a message that will be shown to the user, or a URL and a message (for example a warning that the file has been renamed and the new URL).
  5. Thanks to the message and the "langCode" parameter you can use localized messages instead of having them in English.

So I think that that is the summary of differences showing that the limitations in the previous API (hardcoded error numbers and messages, no info about the current instance...) have been fixed.

Practical example

Maybe that's not enough for you, so here's an example "upload.php" file that you can use to study and adjust your code. This code doesn't save any file, doesn't include any security check, it doesn't check for errors... It's just some basic sample explaining what you can do.

<?php

// This is just a simple example, not to be used in production!!!

// ------------------------
// Input parameters: optional means that you can ignore it, and required means that you
// must use it to provide the data back to CKEditor.
// ------------------------

// Optional: instance name (might be used to adjust the server folders for example)
$CKEditor = $_GET['CKEditor'] ;

// Required: Function number as indicated by CKEditor.
$funcNum = $_GET['CKEditorFuncNum'] ;

// Optional: To provide localized messages
$langCode = $_GET['langCode'] ;

// ------------------------
// Data processing
// ------------------------

// The returned url of the uploaded file
$url = '' ;

// Optional message to show to the user (file renamed, invalid file, not authenticated...)
$message = '';

// In FCKeditor the uploaded file was sent as 'NewFile' but in CKEditor is 'upload'
if (isset($_FILES['upload'])) {
    // ToDo: save the file :-)
    // Be careful about all the data that it's sent!!!
    // Check that the user is authenticated, that the file isn't too big,
    // that it matches the kind of allowed resources...
    $name = $_FILES['upload']['name'];

    // example: Build the url that should be used for this file   
    $url = "/images/" . $name ;
    // Usually you don't need any message when everything is OK.
//    $message = 'new file uploaded';   
}
else
{
    $message = 'No file has been sent';
}
// ------------------------
// Write output
// ------------------------
// We are in an iframe, so we must talk to the object in window.parent
echo "<script type='text/javascript'> window.parent.CKEDITOR.tools.callFunction($funcNum, '$url', '$message')</script>";

?>

If the code is deemed worth, it might be added to the wiki (for example in Custom File Browser, or creating a new article "Custom Uploader") but before doing so I would like to hear a little feedback.

 

Update 10/08/2013:

I've added the missing part to save the file, use it at your own risk. Go to the new post explaining it.

2009/12/06

Simple things are hard to achieve

A very important part of easyUpload is that it must be easy, it should protect the user against unexpected behavior, it should be straight forward and simple, let's see two details:

Tabs

There are no tabs in the dialog. If no image is selected in the editor then it starts with a screen asking the user to select the image that he wants to use, and the options are quite clear: upload from the computer, browse the server, use an url.

In order to hide the tab bar (because in reality the code is splitted in two tabs, but only one of them is visible each time) I had to find a workaround because as I said only one tab is visible, but the code checks for the number of tabs and then it decides to make the tab bar visible.

So this is one possible solution (in the onShow method of the dialog)

this.parts.tabs.hide();

This does indeed hide the tab bar, but the spacing remains there, a better solution is to mark the dialog as "single paged":

this.parts.dialog.addClass("cke_single_page");

This little trick adds the class used by CKEditor to mark a dialog as single paged, so the tabs bar is gone as well as the spacing.

Ok button

If the user browses the server, as soon as he has picked the image the first screen dissapears and then the image properties are shown. The other two options have a button each one to confirm the selection, but there's a problem: the big "OK" button at the end. Users might click on it instead of using the one besides its option, so we need to listen to this button.

That's easy, you just need to add an event listener on the dialog for the 'ok' event.
But wait, it doesn't work!
The dialog keeps showing the dreaded "Some of the options have been changed", not our event handler!

Fortunately the Event system in CKEditor has a nice feature: The ability to specify a priority for our listener so we just need use a priority lower than the default (10) to get the event first. But if you look at the code you'll find that the dialog constructor adds its listener for this event (the one that later will show the alert) with a priority of 0. Oh, no.

Why does it needs to specify 0 as its priority? If it's the first listener that it's being added (and we are talking about the constructor) it should get fired first by default without the need to specify anything and saving those little bytes in the code.

What can we do now?
If we add the listener also with 0 it will be called later anyway :-(
But...
We are talking about javascript, in javascript there are no unsigned integers, just numbers, so this little trick is enough

    this.on( 'ok', function( evt ) {
...
    }, this, null, -1 );

Exactly, by setting our listener with priority -1 we get over the default one and no unexpected prompts will be shown :-)
 

Making options simple for the plain users usually requires a significant ammount of time to find the right parts to tweak, so it isn't strange that as the software becomes bigger there are some rough edges and only by focusing on some narrow part it's possible to improve it at the cost of not working on other improvements.

2009/12/05

File upload in CKEditor

CKEditor redefines the way that dialogs are created by defining them as a hierarchy of "uiElements", each element is defined as a JS object and its type define its behavior.

You can see this if you open the source of any of the dialogs and search for "contents :", there you'll find the structure of the tabs and you can see how the "type" property is used with values such "hbox"/"vbox" (for layout of several elements: horizontal/vertical box), other values that matches typical HTML form elements: "text", "button", "password", "textarea", "checkbox", "radio", "select", "html" (this one allows any kind of html to be used directly), and some special ones: "file", "fileButton". These elements and behaviors are defined in the dialogui plugin.

File upload

When you want to provide the "quickupload" feature you must use the "file" uiElement, this one takes care of creating an iframe whose contents are a form with an <input type="file">. But of course, you must specify the url to POST the file, so this is handled by the fileBrowser plugin. This plugin scans every dialog based on the dialogDefinition event and then adjust the properties so the form is created correctly as well as adjusting the buttons used to launch the file browser. This is done based on the existence of a "filebrowser" property in the element definition.

The current definition of the "file" uiElement has some bugs/problems, and so far I haven't fixed all of them as it took me long enough to understand the behavior and I needed to move forward, but I'll try to look at them in order to finish the easyUpload plugin.

One of the problems is that if a user selects a local file but presses the dialog "OK" button instead of the "upload file" [hey, we're talking about users ;-) ] then the dialog might close without any warning and no file is uploaded. This is due to the fact that the .getValue() function is hardcoded to return an empty string, fortunately it's easy to fix that problem at the initialization of our plugin:

        // Fix file input definition:
        CKEDITOR.ui.dialog.file.prototype.getValue = function()
        {
            return this.getInputElement().$.value;
        };

This only causes a little issue: now if you try to close the dialog with the Cancel button it will always state "Some of the options have been changed..." (quite unfriendly message by the way, it would be much better to state "The file upload has been changed..." using the proper label for each element). So we just need to force the initialization of the "initial value" for the element to match the empty string (as I'll explain next, the initialization code is using the container instead of the real <input> so it's doing wrong things at that time)

        CKEDITOR.ui.dialog.file.prototype.setInitValue = function()
        {
            this._.initValue = '';
        };

Of course these two changes are easy and should be available also in the core code, but I needed them right now, so it's easier to provide the fix in the plugin instead of waiting for the patch to get approved.

Pending issues

In the original easyUpload I added the code (that it was then merged into the core) that instead of just letting the user wait for the upload to finish without any indication that there's something going on, he would see the loading throbber. Of course, it's not really "useful" as it doesn't correctly state how much of the file has been uploaded or how much time remains, to see that kind of info you should use Firefox 3.5 with CKFinder 1.4.1.
And it might be argued that sometimes people report problems because they try to upload a file with a wrongly configured connector and they just see the infinite throbber, but the ones that see that problem are the developers setting up CKEditor and I would argue that they should learn a little about all the tools that are available for developers to find javascript errors, server responses, etc...
But let's get back to the topic: the "please wait" message is still missing, and the aim of the plugin is to be user-friendly so it should get one.

Another friendly feature was the automatic upload of the file as soon as it was selected from their computer instead of having to click on the "upload" button.
First let's understand the 'file' widget; it's quite different from the rest of elements as it does create it's own upload iframe, and that iframe is recreated each time the .reset() method is called on it. That happens when you load the dialog, or when a file is uploaded or when the dialog is closed. So every now and the the iframe with the inner form and the <input type="file"> will be recreated, but currently it doesn't fires any event to the parent window, so it can't be tracked to reassign the needed events on it. Besides that, the creation logic of the uiElements is based on assigning the event listeners required on them after they have been created, but due to the iframe element and the oddities of this element, at the time that those assignments happen the inner frame still isn't ready and instead it tries to work with the container of the iframe (quite useless). So the fix for this means that the 'file' uiElement must keep the list of events that it must assign to the inner <input> and that the iframe must notify to the parent container that it has finished loading so it gets reapplied the desired set of events.
Quite a lot of work for a little detail, but the end user just care to use the program without having to take a semminar about uploading files.

The last difference with regards to FCKeditor is that previously if the user picked a ".doc" file in the image dialog he would get immediately a warning stating that this isn't a recognized image format so that won't be uploaded. Now this check at the client side is missing, so the user will upload the file and it's the server the one that must reject it.
This means two things: poorly written server connectors will allow to upload any kind of files at the image dialog and then users will ask why they have uploaded the .doc and it doesn't work (or even worse: iirc correctly Safari in Mac does allows to use a .pdf as source for an img, so they upload the pdf, put it as source for an image and then only Safari users will see the contents). The second problem is that the user has wasted his time uploading the file to the server instead of getting an immediate feedback stating that the file isn't valid for this dialog.

As you can see, there are some rough edges about this functionality that we should expect to improve in CKEditor as it evolves, and meanwhile I'll try to fix them in the easyUpload plugin.

2009/12/04

Porting easyUpload to CKEditor

A little while back I was tasked to upgrade the easyUpload plugin to CKEditor 3. Due to a broken leg this job has taken too long, but it's finally starting to see the end, and I think that writting some little posts about the problems/workarounds that I've used might be useful for other people trying to write some plugins/customize CKEditor.

Given the new events in CKEditor at first it looked like it would be possible to directly use the existing dialogs and then apply our changes over them (you can look at the api_dialog.html file to see how it's done). That was my idea after writting the original plugin for FCKeditor, I saw that I used a bunch of existing code so having a way to modify the dialog before it's used would be great. But the reality is that in their current state those dialogs can't be customized without writting lots of code (and ugly code) to change the inner logic. I'm gonna talk about the image dialog to explain some of its problems, of course I expect this to change in due course and a future version of the plugin might be as clean as originally envisioned.

In the image dialog you just can't remove the preview or the scripts will fail. The possibility of removing that part hasn't been considered/tested and so the code uses it without first testing for its existence. This is easy to workaround (just some "if"s that you can check when the plugin is releaded) and I think that it might be fixed in the official version. Anyway, this isn't a high priority item because the preview is quite nice in order to verify how the image will look like, but if someone cares to provide a patch to do it the chances will be higher :-)

Lock size & Reset size buttons: They are a single entity without any identifier. It's possible to hide them only after the dialog has been created (vs using the dialogDefinition event to remove them like it's possible for other elements). I think that the solution that I've included (just splitting each button as a single element and reviewing a little some scripts that expected those buttons to always exist) is also easy to add to the general code. For me the important part here is the removal of the "Lock size" button, for plain users that option must be checked always and they shouldn't have to option to turn it off, removing it from the screen just removes a question from their minds.

"Some of the options have been changed...", yep, if you load the image dialog with an existing image loaded and then you try to dismiss the dialog, this prompt will be shown leaving you wondering what has been changed and doubting about closing the dialog or not. This is due to a delayed initialization to avoid an IE7 rendering bug (it seems that IE has several problems with the sizes of the dialogs under certain conditions), and so that initialization needs to be patched with the fixed function.

Considering the number of elements to remove, the scripts that needs to be dynamically patched (as explained above) and then the new elements to add with their logic I decided that it was easier for me to start with a copy of the image dialog and then apply all the changes there instead of having to find ways to override everything that I needed while trying to retain the original functions to perform their work.

These are some of the problems that I've faced, but not the only ones, although they have different origin so they must be handled in a different post.