Search code examples
ckeditor

CKEditor add Format combo items inside context (nested) menu


I am using CKEditor 4.5.7 in one of my project. We have customized Format combo as shown in the below screen shot, the ask is to have all the items appearing inside this combo on right click.

CKEditorFormatCombo

And below is the code for Format combo:

config.format_tags = 'p;h3;h4;pre;ImageInline;ImageCentered;ImageCenteredWithDropShadow;FigureHeading;Equation;EquationDefinition;TableWithoutBorder';
config.format_ImageInline = { name: 'Image inline', element: 'img', attributes: { 'class': 'noborder' } };
config.format_ImageCentered = { name: 'Image centered', element: 'img', attributes: { 'class': 'noborderblock' } };
config.format_ImageCenteredWithDropShadow = { name: 'Image centered drop shadow', element: 'img', attributes: { 'class': 'border' } };
config.format_FigureHeading = { name: 'Figure/Table heading', element: 'p'/*['p', 'td']*/, attributes: { 'class': 'footing' } };
config.format_Equation = { name: 'Equation', element: 'table', attributes: { 'class': 'equation' } };
config.format_EquationDefinition = { name: 'Equation definition', element: 'table', attributes: { 'class': 'where' } };
config.format_TableWithoutBorder = { name: 'Table without border', element: 'table', attributes: { 'class': 'nobordertable' } };

I was able to get them displayed in context menu as shown in below screen shot: CKEditor Context menu

But I am not sure what will be the command name for each one of them. i.e.

command: 'format_ImageCentered' /*I need help here*/ command: 'format_ImageCenteredWithDropShadow' /*I need help here*/

I have already downloaded full source code of CKEditor and gone thru ckeditor\plugins\format\plugin.js but wasn't able to figure out what to specify as command.

Below is my code for customizing Context menu:

var ck_article = CKEDITOR.replace("content", { customConfig: '<config js file path>', bodyClass: '<css class>' });
ck_article.on("instanceReady", function (evt) {
    var editor = evt.editor;        
    /*Code for checking if editor has context menu or not removed for brevity*/
    //... 
    //...
    editor.addMenuGroup('ck_group');

    editor.addMenuItem('bold', {
        label: 'Bold',
        command: 'bold',
        group: 'ck_group'
    });

    editor.addMenuItem('iconselector', {
        label: '...',
        command: 'iconselector',
        group: 'ck_group'
    });

    editor.addMenuItem('numberedlist', {
        label: 'Numbered List',
        command: 'numberedlist',
        group: 'ck_group'
    });

    editor.addMenuItem('bulletedlist', {
        label: 'Bulleted List',
        command: 'bulletedlist',
        group: 'ck_group'
    });

    editor.addMenuItem('link', {
        label: 'Link',
        command: 'link',
        group: 'ck_group'
    });

    editor.addMenuItems({
        formatting: {
            label: 'Formatting',
            group: 'ck_group',
            getItems: function () {
                var selection = editor.getSelection();
                //This is to nest items inside context menu of CKEditor
                return {
                    format_ImageCentered: CKEDITOR.TRISTATE_ON,
                    format_ImageCenteredWithDropShadow: CKEDITOR.TRISTATE_ON
                }
            }
        },

        format_ImageCentered: {
            label: "Image centered",
            group: 'ck_group',
            command: 'format_ImageCentered' /*I need help here*/
        },

        format_ImageCenteredWithDropShadow: {
            label: "Image centered drop shadow",
            group: 'ck_group',
            command: 'format_ImageCenteredWithDropShadow' /*I need help here*/
        }
    });

    editor.contextMenu.addListener(function (element, selection, elementPath) {

        var contentMenuItems = {
            link: CKEDITOR.TRISTATE_ON,
            bold: CKEDITOR.TRISTATE_ON,
            numberedlist: CKEDITOR.TRISTATE_ON,
            bulletedlist: CKEDITOR.TRISTATE_ON,
            iconselector: CKEDITOR.TRISTATE_ON,
            formatting: CKEDITOR.TRISTATE_ON
        };

        if (element.getAscendant('a', true)) {
            //If we are already inside 'a' tag then remove link from Context menu else we will end up with two "Link" menus
            delete contentMenuItems.link
        }

        if ($.trim(selection.getSelectedText()) === '') {
            //If no text is selected then remove bold from context menu
            delete contentMenuItems.bold;
            //contentMenuItems.bold = CKEDITOR.TRISTATE_DISABLED; //This doesn't work as the menu item is disabled but hover effect is still there

            //Similarly remove link if nothing is selected as it will insert hyperlink text as words inside CKEditor
            delete contentMenuItems.link;
            //contentMenuItems.link = CKEDITOR.TRISTATE_DISABLED; //This doesn't work as the menu item is disabled but hover effect is still there
        }

        return contentMenuItems;
    }); 
});

References:

I have used below URL as references:


Solution

  • I ended up creating plugin for each and every option listed in Format combo. If there is any other better way than this then please let me know. I am passing code just in case if someone else stumbles with the similar issue.

    Below screen shot shows how the plugin folder looks inside CKEditor folder: Plugin Folder

    I am pasting just one plugin code as all of plugins have the exact same code the only difference is value for pluginName, which is exact same as folder name:

    //All the files inside folder stating with context_<name> have exact same code except pluginName variable.
    //I need to this to support format inside right click
    (function () {
        "use strict";
        var pluginName = 'contextmenu_tablewithoutborder'; //This name will be used to add to 'config.extraPlugins' string
        var commandName = pluginName;
        // Register the plugin within the editor.
        CKEDITOR.plugins.add(pluginName, {
            // Register the icons. They must match command names.
            icons: pluginName,
            // The plugin initialization logic goes inside this method.
            init: function (editor) {
                // Define an editor command.
                editor.addCommand(commandName, { //Command name must match with name provided in editor.ui.addButton
                    // Define the function that will be fired when the command is executed.
                    exec: function (editor) {
                        if (typeof editor.applyFormatStyles === 'function')
                            editor.applyFormatStyles(pluginName.split('_')[1]);
                        else
                            throw new Error('applyFormatStyles is not defined as function (' + pluginName + ')');
                    }
                });
            }
        });
    })();
    

    I then extended CKEditor and added below methods:

    CKEDITOR.editor.prototype.getFormatStyles = function () {
        var styles = {}
        var editor = this;
        var config = editor.config,
            lang = editor.lang.format;
    
        // Gets the list of tags from the settings.
        var tags = config.format_tags.split(';');
    
        for (var i = 0; i < tags.length; i++) {
            var tag = tags[i];
            var style = new CKEDITOR.style(config['format_' + tag]);
            if (!editor.filter.customConfig || editor.filter.check(style)) {
                styles[tag] = style;
                styles[tag]._.enterMode = editor.config.enterMode;            
            }
        }
    
        return styles;
    }
    
    CKEDITOR.editor.prototype.applyFormatStyles = function (styleName) {
        var editor = this;
        var styles = editor.getFormatStyles();
        editor.focus();
        editor.fire('saveSnapshot');
    
        var style = styles[styleName],
            elementPath = editor.elementPath();
    
        editor[style.checkActive(elementPath, editor) ? 'removeStyle' : 'applyStyle'](style);
    
        // Save the undo snapshot after all changes are affected. (#4899)
        setTimeout(function () {
            editor.fire('saveSnapshot');
        }, 0);
    }
    

    I then modified my config file and added all these plugins as extra plugins:

    CKEDITOR.editorConfig = function (config) {
        var extraPlugins = [];
        //Remove other code for brevity
        //...
        //... 
        config.format_tags = 'p;h3;h4;pre;imageinline;imagecentered;imagecenteredwithdropshadow;figureheading;equation;equationdefinition;tablewithoutborder';
        config.format_imageinline = { name: 'Image inline', element: 'img', attributes: { 'class': 'noborder' } };
        config.format_imagecentered = { name: 'Image centered', element: 'img', attributes: { 'class': 'noborderblock' } };
        config.format_imagecenteredwithdropshadow = { name: 'Image centered drop shadow', element: 'img', attributes: { 'class': 'border' } };
        config.format_figureheading = { name: 'Figure/Table heading', element: 'p'/*['p', 'td']*/, attributes: { 'class': 'footing' } };
        config.format_equation = { name: 'Equation', element: 'table', attributes: { 'class': 'equation' } };
        config.format_equationdefinition = { name: 'Equation definition', element: 'table', attributes: { 'class': 'where' } };
        config.format_tablewithoutborder = { name: 'Table without border', element: 'table', attributes: { 'class': 'nobordertable' } };
    
        var contextmenu_plugins = config.format_tags.split(";");
        for (var i = 0; i < contextmenu_plugins.length; i++) {
            var pluginName = contextmenu_plugins[i];
            extraPlugins.push("contextmenu_{0}".format(pluginName))
        }
    
        config.extraPlugins = extraPlugins.join(',');
    }
    

    And then finally when I create editor I extent the context menu. I could add this logic in plugin.js file but since I wanted plugin.js code to be exact same except a line or two's difference I didn't bother adding it there.

    var ck_article = CKEDITOR.replace("content", { customConfig: '<config js file path>', bodyClass: '<css class>' });
    
    ck_article.on("instanceReady", function (evt) { 
        var editor = evt.editor;
        editor.addMenuGroup('ck_group');
    
        editor.addMenuItem('bold', {
            label: 'Bold',
            command: 'bold',
            group: 'ck_group'
        });
    
        editor.addMenuItem('iconselector', {
            label: '...',
            command: 'iconselector',
            group: 'ck_group'
        });
    
        editor.addMenuItem('numberedlist', {
            label: 'Numbered List',
            command: 'numberedlist',
            group: 'ck_group'
        });
    
        editor.addMenuItem('bulletedlist', {
            label: 'Bulleted List',
            command: 'bulletedlist',
            group: 'ck_group'
        });
    
        editor.addMenuItem('link', {
            label: 'Link',
            command: 'link',
            group: 'ck_group'
        });
    
        editor.addMenuItems({
            formatting: {
                label: 'Formatting',
                group: 'ck_group',
                getItems: function () {
                    var selection = editor.getSelection();
                    //This is to nest items inside context menu of CKEditor
                    var tags = editor.config.format_tags.split(";");
                    var menu_items = {
                    };
                    for (var i = 0; i < tags.length; i++) {
                        menu_items[tags[i]] = CKEDITOR.TRISTATE_ON;
                    }
                    //menu_items - will have object something like below 
                    //{p: 1, h3: 1, h4: 1......}
                    return menu_items;
                }
            },
    
            p: {
                label: "Normal",
                group: 'ck_group',
                command: 'contextmenu_p'
            },
    
            h3: {
                label: "Heading",
                group: 'ck_group',
                command: 'contextmenu_h3'
            },
    
            h4: {
                label: "Sub Heading",
                group: 'ck_group',
                command: 'contextmenu_h4'
            },
    
            pre: {
                label: "Formatted",
                group: 'ck_group',
                command: 'contextmenu_pre'
            },
    
            imageinline: {
                label: "Image inline",
                group: 'ck_group',
                command: 'contextmenu_imageinline'
            },
    
            imagecentered: {
                label: "Image centered",
                group: 'ck_group',
                command: 'contextmenu_imagecentered'
            },
    
            imagecenteredwithdropshadow: {
                label: "Image centered drop shadow",
                group: 'ck_group',
                command: 'contextmenu_imagecenteredwithdropshadow'
            },
    
            figureheading: {
                label: "Figure/Table heading",
                group: 'ck_group',
                command: 'contextmenu_figureheading'
            },
    
            equation: {
                label: "Equation",
                group: 'ck_group',
                command: 'contextmenu_equation'
            },
    
            equationdefinition: {
                label: "Equation definition",
                group: 'ck_group',
                command: 'contextmenu_equationdefinition'
            },
    
            tablewithoutborder: {
                label: "Table without border",
                group: 'ck_group',
                command: 'contextmenu_tablewithoutborder'
            }
        });
    
        editor.contextMenu.addListener(function (element, selection, elementPath) {
    
            var contentMenuItems = {
                link: CKEDITOR.TRISTATE_ON,
                bold: CKEDITOR.TRISTATE_ON,
                numberedlist: CKEDITOR.TRISTATE_ON,
                bulletedlist: CKEDITOR.TRISTATE_ON,
                iconselector: CKEDITOR.TRISTATE_ON,
                formatting: CKEDITOR.TRISTATE_ON
            };
    
            if (element.getAscendant('a', true)) {
                //If we are already inside 'a' tag then remove link from Context menu else we will end up with two "Link" menus
                delete contentMenuItems.link
            }
    
            if ($.trim(selection.getSelectedText()) === '') {
                //If no text is selected then remove bold from context menu
                delete contentMenuItems.bold;
    
                //Similarly remove link if nothing is selected as it will insert hyperlink text as words inside CKEditor
                delete contentMenuItems.link;
            }
    
            return contentMenuItems;
        });
    });
    

    I know there is too much of code but I wasn't able to find any other simple way to do this.

    When all done this is how it looks: CKEditor Nested Context Menu

    Quick recap:

    • Created plugin for each item appearing in Format combo
    • Only change in all the plugin.js file is pluginName (it is name of the folder)
    • Extend CKEditor and add applyFormatStyles method which will be invoked by plugin.js file
    • In your config file add all these as extra plugins
    • And on creation of CKEditor instance extend context menu