Adding to the CKEditor context menu

We are using the excellent CKEditor in some of our Visualforce pages now, partly to get around the problem that standard rich text fields error when they are re-rendered. But explicitly adding the editor also makes it cleaner to leverage its many features and the one described here is the ability to extend the context (right click) menu. I couldn’t find a simple example, so I hope this post will act as that for others.

The JavaScript code is below and works on this text area:

<apex:inputTextArea value="{!ref.Rtf__c}" styleClass="ckeditor" richText="false"/>

The purpose of the menu is to allow fragments of text to be inserted when selected via the context menu. (The actual JavaScript menu code is much longer and generated by Apex code using data returned by describe calls; it is also loaded from a static resource so that it can be cached by the browser.)

Here is a screen shot from the code posted here:

Menu (1)

A brief explanation of the code is that the menu items either return other menu items (when they are the root menu or a sub-menu) or reference a command (when they are a leaf action). The menu groups add dividing lines into the menus. The editor is added based on the ckeditor class name.

The code:

<script src="//cdn.ckeditor.com/4.5.9/standard/ckeditor.js"></script>
<script>

// Menu code
function addMergeFieldMenu(event) {
    var e = event.editor;
    
    // Commands
    e.addCommand("cv_claimant_Birthdate", {
        exec: function(e) {
            e.insertText("\{\!claimant.Birthdate\}");
        }
    });
    e.addCommand("cv_claimant_Name", {
        exec: function(e) {
            e.insertText("\{\!claimant.Name\}");
        }
    });
    e.addCommand("cv_claim_Name", {
        exec: function(e) {
            e.insertText("\{\!claim.Name\}");
        }
    });
    e.addCommand("cv_claim_CreatedDate", {
        exec: function(e) {
            e.insertText("\{\!claim.CreatedDate\}");
        }
    });
    
    // Listener
    e.contextMenu.addListener(function(element, selection) {
        return {
            cv: CKEDITOR.TRISTATE_ON
        };
    });
    
    // Menu Groups; different numbers needed for the group separator to show
    e.addMenuGroup("cv", 500);
    e.addMenuGroup("cv_people", 100);
    e.addMenuGroup("cv_claim", 200);
    
    // Menus (nested)
    e.addMenuItems({
        // Top level
        cv: {
            label: "Insert Merge Field",
            group: "cv",
            getItems: function() {
                return {
                    cv_claimant: CKEDITOR.TRISTATE_OFF,
                    cv_claim: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // One sub-menu
        cv_claimant: {
            label: "Claimant Person (claimant)",
            group: "cv_people",
            getItems: function() {
                return {
                    cv_claimant_Birthdate: CKEDITOR.TRISTATE_OFF,
                    cv_claimant_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claimant_Birthdate: {
            label: "Birthdate (Birthdate: date)",
            group: "cv_people",
            command: "cv_claimant_Birthdate"
        },
        cv_claimant_Name: {
            label: "Full Name (Name: string)",
            group: "cv_people",
            command: "cv_claimant_Name"
        },
        // Another sub-menu
        cv_claim: {
            label: "Claim (claim)",
            group: "cv_claim",
            getItems: function() {
                return {
                    cv_claim_CreatedDate: CKEDITOR.TRISTATE_OFF,
                    cv_claim_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claim_Name: {
            label: "Claim Number (Name: string)",
            group: "cv_claim",
            command: "cv_claim_Name"
        },
        cv_claim_CreatedDate: {
            label: "Created Date (CreatedDate: datetime)",
            group: "cv_claim",
            command: "cv_claim_CreatedDate"
        },
    });
}

// Add and configure the editor
function addEditor() {
    CKEDITOR.replaceAll('ckeditor');
    for (var name in CKEDITOR.instances) {
        var e = CKEDITOR.instances[name];
        e.config.height = 400;
    }
    CKEDITOR.on('instanceReady', addMergeFieldMenu);
}
addEditor();

</script>