Upgrading FCKeditor 2.x to CKEditor 3.x including plugins

Upgrading FCKEditor 2.x with custom plugins to CKEditor 3.x is a challenging task because so much has changed but it is possible. I'd like to share here few experiences from the upgrade and show how to map the most important API use cases from the old to the new version and ease the migration by first introducing a facade for (F)CKEditor APIs.

The differences

CKEditor 3.x is a great improvement over FCKEditor 2.x, mainly thanks to its modularity and much improved - though sometimes still lacking - documentation. There are large differences between the two:
  1. The API has changed considerably
  2. The configuration options have changed, fortunately the develpers have provided a configuration mapping document to help with the upgrade
  3. Plugin dialogs don't use HTML anymore but are defined in JavaScript, dialogs aren't iframes anymore
  4. The HTML structure has changed a lot and thus it's practically impossible to migrate a custom skin to the new version, you have to re-create it from scratch
  5. Localization keys are different too so it takes an effort to migrate a custom translation

Upgrading the editor

Upgrading the editor was quite easy for even though some things have changed, the way it is used and configured is pretty much the same. We use (F)CKEditor's PHP integration wrapped with our own class so we just needed to re-write the wrapper to support the new version, which was quite simple.

Upgrading/migrating plugins

Due to the numerous changes (dialog definition, editor API, localization, dropped iframes) it isn't trivial to migrate a plugin. You've a couple of options:
  1. Re-write the plugin from scratch, perhaps based on an existing CKEditor plugin
  2. Drop the plugin and use a similar one for CKEditor - that was our case with the Smiley plugin, for the 3.x version is much more configurable and we therefore haven't needed to customize it anymore
    • "The architecture in CKEditor allows for very flexible customization of the contents of the dialogs without any need to change the source files." - see the Dialog Customization doc
  3. Reuse your plugin's HTML via a custom dialog or an iframe dialog, replacing FCKEditor API calls with their CKEditor versions
We've applied all three approaches but mostly the last one for lot of our plugins actually just delegated the work to another page using an iframe. In this case you need to handle localization of the page for the method FCKLanguageManager.TranslatePage doesn't exist anymore and you need to map the old API calls to the new one.

Mapping FCKEditor 2.x API to CKEditor 3.x

To ease the migration from FCKEditor to CKEditor and to protect our code from similar changes in the future I've created a facade, which provides a simplified API to our code and internally calls the proper FCKEditor or CKEditor functions.

(I should mention that I'm no expert on CKEditor and especially CKEditor so there are certainly better ways to do many things. Also, the facade covers only the needs we had and not the full range of (F)CKEditor's functionality.)

An overview of the facade

  • The facade provides two public classes:
    • CKEDITOR_FACADE - the facade entry point, provides e.g. the functions getEditorContent, closeDialog, initialized
    • CKEDITOR_FACADE.editorContent - API for manipulating the content of the rich text editor, provides e.g. the functions getSelectedElement, getSelectedText, insertElement, updateElement, insertElementHtml
  • There are two implementations of the facade and its editorContent, one for each supported editor (FCKEditor 2.x, CKEditor 3.x), which one to use is detected automatically based on the available window properties set by the editors
  • The facade is only expected to be used and only works from the context of an editor popup dialog or a page included in a dialog via an iframe

How to use the facade

1. Include the javascript file:
 <script type='text/javascript' src='/path/to/ckeditor_facade.js'></script>
2. Verify that it is initialized, i.e. one of the 2 implementations is available:
 var editorContent = CKEDITOR_FACADE.initialized()? CKEDITOR_FACADE.getEditorContent() : null;
3. Use it! Examples:
  • Close the dialog:
    
    if (CKEDITOR_FACADE.initialized()) { CKEDITOR_FACADE.closeDialog(); }
    
  • Insert a new element:
    
    var oLink = editorContent.insertElement('a', 'click me!');
    editorContent.updateElement(oLink, {href: 'http://goo.gl/', target:'_blank'});
    
  • Update the selected element (of a particular type):
    
    var oLink = editorContent.getSelectedElement('a');
    editorContent.updateElement(oLink, {href: newUri});
    
The facade exposes only a small subset of the available functionality but it was enough for us.

The facade's code - ckeditor_facade.js

The interface of the facade itself:


if (typeof(window.CKEDITOR_FACADE) == 'undefined' )
{
   window.CKEDITOR_FACADE = (function()
   {
        var _impl = null;

var CKEDITOR_FACADE = /** @lends CKEDITOR_FACADE */ { _initialize : function(facadeImplementation) {_impl = facadeImplementation;} ,

/** * The callbeck will be called with an instance of * CKEDITOR_FACADE.editorContent when the dialog OK button is pressed. * @param {function} * @see CKEDITOR_FACADE.editorContent */ setOnOkCallback : function(onOkCallback) {_impl.setOnOkCallback(onOkCallback)} , getEditorContent : function() {return _impl.getEditorContent()} , closeDialog : function() {_impl.closeDialog()} , initialized : function() {return _impl != null} };

return CKEDITOR_FACADE; })();


The interface of the editorContent facade:



/** * Facade for an editor instance available in the callbacks, * used to add content to the editor and to update existing * (selected) content. * * If it doesn't support something you need then you should add * an appropriate method. * * Note: The actual implementations are provided by subclasses. */ CKEDITOR_FACADE.editorContent = function(wrappedEditor) { this.wrappedEditor = wrappedEditor; }

CKEDITOR_FACADE.editorContent.prototype = { /** * Insert HTML representing a single element * into the current selection or where the cursor is. * @param {string} html code * @return nothing */ insertElementHtml : function(html) {}, /** * Get the currently selected element (or the one selection is inside of). * @param {string} elementName - name of the html element, e.g. 'a' * @return {Object} the element or null if not selected */ getSelectedElement : function(elementName) {}, /** * Update the given element with the new attributes, * removing all existing ones first. * @param {Object} element the existing element * @param {Object} newAttributes An object containing the names and * values of the attributes. * @return the input element * @example * var element = editorContent.getSelectedElement(); * <strong>editorContent.updateElement(element, { * 'class' : 'myClass', * 'title' : 'This is an example' })</strong>; */ updateElement : function(element, newAttributes) {}, /** * Insert a new element of the given name and return it. * @param {string} elementName - name of the html element, e.g. 'a' * @param {string} (optional) inner html to be set * @return {Object} the element created */ insertElement : function(elementName, innerHtml) {}, /** Returns the currently selected text or null */ getSelectedText : function() {} }

CKEditor 3.x implementation

Facade's CKEditor implementation
  • we first define the class, its constructor taking the global CKEDITOR object
  • then we define the static method which tries to find out whether CKEDITOR instance is somewhere around and if it is then it instantiates this facade implementation
  • the method getEditorContent tries to locate the actual instance of CKEditor editor to be used and instantiates an editorContent for it.

    CKEDITOR_FACADE.facade_CKEditor35Impl = function(editorClass) {
        this.CKEDITOR_SINGLETON = editorClass;
    }

/** * Create an instance if the base editor is defined. * @private * @static */ CKEDITOR_FACADE.facade_CKEditor35Impl.instantiateIfAvailable = function() { var ckeditorDefined = typeof(window.parent.CKEDITOR) != 'undefined'; if (!ckeditorDefined) return null;

var CKEDITOR = window.parent.CKEDITOR; var insideCkDialog = ckeditorDefined? (typeof(CKEDITOR.dialog) != 'undefined') : false;

if (insideCkDialog) return new CKEDITOR_FACADE.facade_CKEditor35Impl(CKEDITOR); else return null; }

CKEDITOR_FACADE.facade_CKEditor35Impl.prototype = (function() {

return /** @lends CKEDITOR_FACADE.facade_CKEditor35Impl.prototype */ { editorContent : null , getEditorContent : function(){ if (this.editorContent == null) { var editorSingleton = this.CKEDITOR_SINGLETON; // inside callbacks this referes to a dialog var dialog = editorSingleton.dialog.getCurrent(); var editorInstance = dialog._.editor; var editorContent = new CKEDITOR_FACADE.editorContent_CKEditor35Impl(editorInstance, editorSingleton); this.editorContent = editorContent; } return this.editorContent; } , setOnOkCallback : function(onOkCallback) { // Copy fields of this: inside callbacks this referes to dialog var editorSingleton = this.CKEDITOR_SINGLETON; var editorContent = this.getEditorContent();

var okListener = function(event) { // Note: this is an instance of CKEDITOR.dialog onOkCallback(editorContent); editorSingleton.dialog.getCurrent().removeListener("ok", okListener); };

editorSingleton.dialog.getCurrent().on("ok", okListener); // only defined inside a dialog window } , closeDialog : function() { this.CKEDITOR_SINGLETON.dialog.getCurrent().hide(); } }; })();


EditorContent's CKEditor implementation
  • the implementation extends CKEDITOR_FACADE.editorContent and therefore its constructor calls first the parent's constructor and then it stores a reference to the global CKEDITOR object

    /**
     * NEW CKEDITOR 3.5 IMPLEMENTATION OF CKEDITOR_FACADE.editorContent
     */
    CKEDITOR_FACADE.editorContent_CKEditor35Impl = function(wrappedEditor, editorSingleton)
    {
        CKEDITOR_FACADE.editorContent.call(this, wrappedEditor);
        this.CKEDITOR_SINGLETON = editorSingleton;
    }

CKEDITOR_FACADE.editorContent_CKEditor35Impl.prototype = { insertElementHtml : function(elementHtml) { var element = this.CKEDITOR_SINGLETON.dom.element.createFromHtml(elementHtml); this.wrappedEditor.insertElement(element); } , /** * Return the (surrounding) selected element of the given name or null. * * Copied from CKEDITOR.plugins.link.getSelectedLink and adjusted. */ getSelectedElement : function(elementName) { try { var selection = this.wrappedEditor.getSelection(); if ( selection.getType() == this.CKEDITOR_SINGLETON.SELECTION_ELEMENT ) { var selectedElement = selection.getSelectedElement(); if ( selectedElement.is(elementName) ) return selectedElement; }

// Handle cases like "[<a href="...">li]nk</a>" ([..] is the selection) var range = selection.getRanges(true)[0]; range.shrink( this.CKEDITOR_SINGLETON.SHRINK_TEXT ); var root = range.getCommonAncestor(); return root.getAscendant(elementName, true); } catch( e ) {return null;} } , updateElement : function(element, newAttributes) { // element shall be CKEDITOR.dom.element element.removeAttributes(newAttributes);

// !!! It seems that CKEditor ignores href and only // changes the link's href according to the attribute data-cke-saved-href if(newAttributes.href) { newAttributes['data-cke-saved-href'] = newAttributes.href; } element.setAttributes(newAttributes);

return element; } , insertElement : function(elementName, innerHtml) { var element = new this.CKEDITOR_SINGLETON.dom.element(elementName); if (typeof(innerHtml) != 'undefined') { element.setHtml(innerHtml); } this.wrappedEditor.insertElement(element); return element; } , getSelectedText : function() { try { var mySelection = this.wrappedEditor.getSelection();

if (this.CKEDITOR_SINGLETON.env.ie) { mySelection.unlock(true); return mySelection.getNative().createRange().text.toString(); } else { return mySelection.getNative().toString(); }

} catch( e ) {return null;} } }

FCKEditor 2.x implementation

Facade's FCKEditor implementation


    CKEDITOR_FACADE.facade_FCKEditor2xImpl = function(editorClass) {
        this.FCKEDITOR = editorClass;
    }

/** * Create an instance if the base editor is defined. * @private * @static */ CKEDITOR_FACADE.facade_FCKEditor2xImpl.instantiateIfAvailable = function() { var fckDialogWindow = window.parent; var editorInstance = null; // window with url similar to /editor/fckeditor.html?InstanceName=raw_body&Toolbar=Default if (typeof fckDialogWindow.oEditor != 'undefined') { editorInstance = window.parent.oEditor; } else if (typeof(fckDialogWindow.InnerDialogLoaded) == 'function') // try to load it ... { editorInstance = fckDialogWindow.InnerDialogLoaded(); }

return editorInstance? new CKEDITOR_FACADE.facade_FCKEditor2xImpl(editorInstance) : null; }

CKEDITOR_FACADE.facade_FCKEditor2xImpl.prototype = { getEditorContent : function() { return new CKEDITOR_FACADE.editorContent_FCKEditor2xImpl(this.FCKEDITOR); } , setOnOkCallback : function(onOkCallback) { //@todo implement this if it is to be used } , closeDialog : function() { if (typeof(window.parent.dialog) != 'undefined') { window.parent.dialog.Cancel(); // normal situation } else if (typeof(window.parent.Cancel) == 'function') { window.parent.Cancel(); }

} }


EditorContent's FCKEditor implementation


    /**
     * OLD FCKEDITOR 2.x IMPLEMENTATION OF CKEDITOR_FACADE.editorContent
     */
    CKEDITOR_FACADE.editorContent_FCKEditor2xImpl = function(wrappedEditor)
    {
        CKEDITOR_FACADE.editorContent.call(this, wrappedEditor);
    }

CKEDITOR_FACADE.editorContent_FCKEditor2xImpl.prototype = { _getDialog : function() { //The dialog window is an iframe with src=fckdialog.html var dialogWindow = window.parent; while(typeof(dialogWindow.FCKDialog) == 'undefined' && dialogWindow.parent != dialogWindow) { dialogWindow = dialogWindow.parent; } return dialogWindow; } , insertElementHtml : function(elementHtml) { this.wrappedEditor.FCK.InsertHtml(elementHtml); } , //@see fckeditor/editor/dialog/fck_link/fck_link.js getSelectedElement : function(elementName) { var oLink = this._getDialog().Selection.GetSelection().MoveToAncestorNode(elementName.toUpperCase()) ; if (oLink) this.wrappedEditor.FCK.Selection.SelectNode(oLink) ; return oLink; } , insertElement : function(elementName, innerHtml) { var element = this.wrappedEditor.FCK.InsertElement(elementName); if(typeof(innerHtml) != 'undefined') { element.innerHTML = innerHtml; } return element; } , updateElement : function(element, newAttributes) { // element shall be an HTMLAnchorElement /*var currentAttributes = new Array(); for (i = element.attributes.length - 1; i >= 0; --i) { var attName = element.attributes[i].name; element.removeAttribute( attName, 0 ) ; // 0 : Case Insensitive }*/ var caseSensitive = 0; // not c.s.

// COPIED PATCH (applies to links): Save the innerHTML (IE changes it if it is like an URL). var storedInnerHtml = null; if (element.tagName.toLowerCase() == 'a') storedInnerHtml = element.innerHTML;

for ( var name in newAttributes ) { var attValue = newAttributes[ name ];

if ( attValue == null || attValue.length == 0 ) element.removeAttribute( name, caseSensitive ) ; else element.setAttribute( name, attValue, caseSensitive ); }

// COPIED PATCH: restore html if (storedInnerHtml) element.innerHTML = storedInnerHtml; // Set (or restore) the innerHTML

return element; } , getSelectedText : function() { try { var editorWindow = this.wrappedEditor.FCK.EditorWindow; var selection = (editorWindow.getSelection ? editorWindow.getSelection() : editorWindow.selection);

if (selection.createRange) { return selection.createRange().text.toString(); } else { return selection.toString(); } } catch( e ) {return null;} } }

Instantiation of the proper implementation

The instantiation of the proper implementation - FCK or CK - happens at the very end of the file (after all the classes have been defined):


    CKEDITOR_FACADE._initialize((function ()
        {
            var error = "CKEDITOR_FACADE: Neither CKEditor 3.x nor FCKEditor 2.x instance found. \
    Make sure that this is called from a child dialog window/iframe of a window containing (F)CKEditor.";

try { // ! The order (ckeditor 1st, fckeditor 2nd) is important if you use // both on the same page for only ckeditor checks for being in ckeditor dialog window var facade = CKEDITOR_FACADE.facade_CKEditor35Impl.instantiateIfAvailable();

// Try the old implementation if the new one unavailable: if (facade == null) { facade = CKEDITOR_FACADE.facade_FCKEditor2xImpl.instantiateIfAvailable(); }

// Fail if neither new nor old (F)CKEditor found if (facade == null) { // fireBug log if (typeof(console) != 'undefined') console.error(error+" This window:", window); else throw new Error(error); }

return facade; } catch(e) { alert(error+" Error:"+e); return null; } }) ());

} /* if CKEDITOR undefined */

Re-implementation of FCKEditor's utility methods

FCKEditor had some global utility functions that your code may be using, we needed one - GetE:


if (typeof(GetE) != 'function')
{
    /** Shorthand method from fck_dialog_common.js */
    function GetE( elementId )
    {
        return document.getElementById( elementId )  ;
    }
}


---

Tags: library


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob