Add custom plugin to CKEditor toolbar 

As we know, Liferay 7 provides different WYSIWYG editors the portal. By default, in Liferay 7 AlloyEditor is available in the portal. We can configure Liferay to use CKEditor by setting following properties in portal-ext.properties file for different portlets. In this blog we will set web content portlet to use CKEditor instead of AlloyEditor

editor.wysiwyg.default=ckeditor
editor.wysiwyg.portal-impl.portlet.ddm.text_html.ftl=ckeditor

CKEditor has different toolbars that contains features like text formatting, text selection, image selection etc.  These features are highly configurable, however there may be cases where configuration isn't enough to meet the requirement.  In such cases we can add the custom plugin in the toolbar and provide your own implementation.  In these cases, we can get the editor instance  and update it by adding out own plugin using the liferay-util:dynamic-include JavaScript extension point. It injects JavaScript code right after the editor instantiation to configure/change the editor.

1. Create a JS file for your plugin functionality and put it under META-INF folder. This file will be registered in your module. The extension point injects the JavaScript code right after editor is initialized. Here we are adding the insert snippet plugin where on click of it a static text will be added to the editor's content.  

(function() {
    CKEDITOR.plugins
            .add(
                    'insertsnippet',
                    {
                        init : function(editor) {
                            var instance = this;

                            editor.addCommand('insertsnippet', {
                                canUndo : false,
                                exec : function(editor, callback) {
                                    instance._onSelectedContentChange(editor,
                                            callback);
                                }
                            });

                            if (editor.ui.addButton) {
                                editor.ui
                                        .addButton(
                                                'InsertSnippet',
                                                {
                                                    command : 'insertsnippet',
                                                    icon : '/o/custom-ckeditor-plugin/js/assets/journal_content.png',
                                                    label : 'Insert Snippet'
                                                });
                            }

                        },

                        _onSelectedContentChange : function(editor, callback) {
                            var instance = this;

                            var selectedItem = '{"content": "<p>first text</p><p>second text</p>"}'
                            var data = instance._getData(selectedItem);

                            if (data) {
                                var content = data.content;
                                editor.insertHtml(content,'unfiltered_html');
                                editor.focus();
                            }
                        },
                        
                        _getData : function(selectedItem) {
                            var data = '';

                            try {
                                data = JSON.parse(selectedItem)
                            } catch (e) {
                            }

                            return data;
                        },
                    });
})();

Liferay will inject this JavaScript code in ckEditor.jsp

2. Create a module that can register your new JS file and inject it into your editor instance.

3. Create the java class with name starting with CKEditor and and end with DynamicInclude. In our case we will name it as CKEditorCustomPluginDynamicInclude.java and it should implement the DynamicInclude interface.  In this class you will instantiate your bundle's context,  register the editor you are customizing into Dynamic include registry and also inject the content of your custom plugin file into the editor.  More info

@Component(immediate = true, service = DynamicInclude.class)
public class CKEditorCustomPluginDynamicInclude implements DynamicInclude {

    @Override
    public void include(HttpServletRequest request, HttpServletResponse response, String key) throws IOException {

        Bundle bundle = _bundleContext.getBundle();

        URL entryURL = bundle.getEntry("/META-INF/resources/js" + "/custom_ckeditor_plugin.js");
        StreamUtil.transfer(entryURL.openStream(), response.getOutputStream(), false);
    }

    @Override
    public void register(DynamicIncludeRegistry dynamicIncludeRegistry) {
        dynamicIncludeRegistry.register("com.liferay.frontend.editor.ckeditor.web#ckeditor#onEditorCreate");
    }

    @Activate
    protected void activate(BundleContext bundleContext) {
        _bundleContext = bundleContext;
    }

    private BundleContext _bundleContext;
}

 

4. Now we got our custom plugin code injected in the editor, we have to configure this plugin into editor's toolbar.   More info

@Component(immediate = true, property = { "editor.name=ckeditor",
        "service.ranking:Integer=1000" }, service = EditorConfigContributor.class)
public class CKEditorCustomPluginConfigContributor extends BaseEditorConfigContributor {

    @Override
    public void populateConfigJSONObject(JSONObject jsonObject, Map<String, Object> inputEditorTaglibAttributes,
            ThemeDisplay themeDisplay, RequestBackedPortletURLFactory requestBackedPortletURLFactory) {

        addPluginToCkEditor(jsonObject);
    }
    
    /**
     * This method adds the insert snippet plugin to ckeditor toolbar 
     * 
     * @param jsonObject
     */
    private void addPluginToCkEditor(JSONObject jsonObject) {

        String extraPlugins = jsonObject.getString("extraPlugins");

        if (Validator.isNotNull(extraPlugins)) {
            extraPlugins = extraPlugins + ",insertsnippet";
            jsonObject.put("extraPlugins", extraPlugins);
        }

        setInsertSnippetButtonForToolbar(jsonObject, "toolbar_liferay");
        setInsertSnippetButtonForToolbar(jsonObject, "toolbar_liferayArticle");
    }

    /**
     * This method adds insert snippet button into the section where we have image selector button   
     * 
     * @param jsonObject
     * @param toolbarName
     */
    private void setInsertSnippetButtonForToolbar(JSONObject jsonObject, String toolbarName) {

        JSONArray toolbar = jsonObject.getJSONArray(toolbarName);
        JSONArray imageSelectorSection = getImageSelectorSection(toolbar);

        if (imageSelectorSection != null) {
            imageSelectorSection.put("InsertSnippet");
        }

        jsonObject.put(toolbarName, toolbar);
    }

    /**
     * This method returns the json array containing the image selector button
     * 
     * 
     * @param toolbar
     * @return
     */
    private JSONArray getImageSelectorSection(JSONArray toolbar) {
        JSONArray imageSelectorSection = null;
        for (int i = 0; i < toolbar.length(); i++) {
            JSONArray array = toolbar.getJSONArray(i);
            if (array != null && hasImageSelector(array)) {
                imageSelectorSection = array;
                break;
            }
        }

        return imageSelectorSection;
    }

    /**
     * This method returns true if image selector button is available in given array
     * 
     * @param array
     * @return
     */
    private boolean hasImageSelector(JSONArray array) {
        boolean hasImageSelector = Boolean.FALSE;
        for(int i=0; i< array.length(); i++) {
            if(StringUtil.equals(array.getString(i), "ImageSelector")) {
                hasImageSelector = Boolean.TRUE;
                break;
            }
        }
        return hasImageSelector;
    }
}

 

5. Now deploy the module and your custom plugin should be available in CKEditor.