AEM 61 - TouchUI Rich Text Editor Color Picker Plugin

Goal


Color Picker Plugin extension for RTE (Rich Text Editor) in 61 Touch UI. Click here for Color Picker widget documentation

Demo shows dialog of foundation text component (/libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins) modified to add the color picker config. This is just for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered...

This is a better implementation of check this post

For Classic UI check this post

For AEM 62 Dialog RTE Color Picker Plugin - check this post

Demo | Package Install


Configuration




Picker with Free Style Palette



Palette Edit Mode




RTE text with Color applied



Remove Color



Solution


1) Login to CRXDE Lite http://localhost:4502/crx/de, add nt:folder /apps/touchui-rte-color-picker-plugin

2) To show the color picker in a dialog create /apps/touchui-rte-color-picker-plugin/color-picker-popover of type sling:Folder and /apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog of type nt:unstructured

3) XML representation of /apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog and structure in CRXDE Lite


<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Color Picker"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
            margin="{Boolean}false"/>
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <picker
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/foundation/form/colorpicker"
                        classicPaletteType="{Boolean}true"
                        editType="{Boolean}true"
                        fieldLabel="Select Color"
                        name="./color">
                        <colors jcr:primaryType="nt:unstructured">
                            <red
                                jcr:primaryType="nt:unstructured"
                                name="Red"
                                value="#FF0000"/>
                            <green
                                jcr:primaryType="nt:unstructured"
                                name="Green"
                                value="#00FF00"/>
                            <blue
                                jcr:primaryType="nt:unstructured"
                                name="Blue"
                                value="#0000FF"/>
                            <black
                                jcr:primaryType="nt:unstructured"
                                name="Black"
                                value="#000000"/>
                            <brown
                                jcr:primaryType="nt:unstructured"
                                name="Brown"
                                value="#996633"/>
                            <orange
                                jcr:primaryType="nt:unstructured"
                                name="Orange"
                                value="#FF7F00"/>
                            <purple
                                jcr:primaryType="nt:unstructured"
                                name="Purple"
                                value="#7F007F"/>
                            <yellow
                                jcr:primaryType="nt:unstructured"
                                name="Yellow"
                                value="#FFFF00"/>
                        </colors>
                    </picker>
                    <add
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/foundation/button"
                        class="coral-Button--primary"
                        id="EAEM_CP_ADD_COLOR"
                        text="Add Color"/>
                    <remove
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/foundation/button"
                        class="coral-Button--warning"
                        id="EAEM_CP_REMOVE_COLOR"
                        text="Remove Color"/>
                </items>
            </column>
        </items>
    </content>
</jcr:root>

4) Colors shown in picker dialog are added in /apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog/content/items/column/items/picker/colors

5) Create clientlib (cq:ClientLibraryFolder) /apps/touchui-rte-color-picker-plugin/clientlib set property categories to rte.coralui2 and dependencies to [underscore]

6) Create file (nt:file) /apps/touchui-rte-color-picker-plugin/clientlib/js.txt, add the following

                   color-picker-plugin.js

7) Create file (nt:file) /apps/touchui-rte-color-picker-plugin/clientlib/color-picker-plugin.js, add the following code

(function ($, $document, Handlebars) {
    "use strict";

    var _ = window._,
        Class = window.Class,
        CUI = window.CUI,
        REQUESTER = "requester",
        GROUP = "experience-aem",
        COLOR_PICKER_FEATURE = "colorPicker",
        COLOR_PICKER_DIALOG = "colorPickerDialog",
        DIALOG_URL = "/apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog",
        PICKER_NAME_IN_POPOVER = "color",
        EAEMCuiToolbarBuilder,
        EAEMColorPickerPluginDialog,
        EAEMDialogManager,
        EAEMToolkitImpl,
        EAEMColorPickerPlugin,
        EAEMColorPickerCmd;

    function getUISetting() {
        return GROUP + "#" + COLOR_PICKER_FEATURE;
    }

    //extend the toolbar builder to register plugin icon in fullscreen mode
    EAEMCuiToolbarBuilder = new Class({
        toString: "EAEMCuiToolbarBuilder",

        extend: CUI.rte.ui.cui.CuiToolbarBuilder,

        _getUISettings: function (options) {
            var uiSettings = this.superClass._getUISettings(options),
                toolbar = uiSettings.fullscreen.toolbar,
                feature = getUISetting();

            if (toolbar.indexOf(feature) === -1) {
                toolbar.splice(3, 0, feature);
            }

            if (!this._getClassesForCommand(feature)) {
                //.coral-ColorPicker-button
                this.registerAdditionalClasses(feature, "coral-Icon coral-Icon--textColor");
            }

            return uiSettings;
        }
    });

    //popover dialog hosting iframe
    EAEMColorPickerPluginDialog = new Class({
        extend: CUI.rte.ui.cui.AbstractBaseDialog,

        toString: "EAEMColorPickerPluginDialog",

        getDataType: function () {
            return COLOR_PICKER_DIALOG;
        }
    });

    //extend the CUI dialog manager to register popover dialog
    EAEMDialogManager = new Class({
        toString: "EAEMDialogManager",

        extend: CUI.rte.ui.cui.CuiDialogManager,

        create: function (dialogId, config) {
            if (dialogId !== COLOR_PICKER_DIALOG) {
                return this.superClass.create.call(this, dialogId, config);
            }

            var context = this.editorKernel.getEditContext(),
                $container = CUI.rte.UIUtils.getUIContainer($(context.root)),
                dialog = new EAEMColorPickerPluginDialog();

            dialog.attach(config, $container, this.editorKernel, true);

            return dialog;
        }
    });

    //extend the toolkit implementation for custom toolbar builder and dialog manager
    EAEMToolkitImpl = new Class({
        toString: "EAEMToolkitImpl",

        extend: CUI.rte.ui.cui.ToolkitImpl,

        createToolbarBuilder: function () {
            return new EAEMCuiToolbarBuilder();
        },

        createDialogManager: function (editorKernel) {
            return new EAEMDialogManager(editorKernel);
        }
    });

    CUI.rte.ui.ToolkitRegistry.register("cui", EAEMToolkitImpl);

    EAEMColorPickerPlugin = new Class({
        toString: "ColorPickerDialogPlugin",

        extend: CUI.rte.plugins.Plugin,

        pickerUI: null,

        getFeatures: function () {
            return [ COLOR_PICKER_FEATURE ];
        },

        initializeUI: function (tbGenerator) {
            var plg = CUI.rte.plugins;

            if (!this.isFeatureEnabled(COLOR_PICKER_FEATURE)) {
                return;
            }

            this.pickerUI = tbGenerator.createElement(COLOR_PICKER_FEATURE,
                this, true, "Select Color");

            tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 120);
        },

        execute: function (id, value, envOptions) {
            var ek = this.editorKernel,
                dm = ek.getDialogManager(),
                $popover, dialog, context = envOptions.editContext;

            if(!isValidSelection()){
                return;
            }

            var dialogConfig = {
                parameters: {
                    "command": getUISetting()
                }
            };

            dialog = this.dialog = dm.create(COLOR_PICKER_DIALOG, dialogConfig);

            dialog.restoreSelectionOnHide = false;

            dm.prepareShow(this.dialog);

            dm.show(this.dialog);

            $popover = this.dialog.$dialog.find(".coral-Popover-content");

            var selection = CUI.rte.Selection.createProcessingSelection(context),
                tag = CUI.rte.Common.getTagInPath(context, selection.startNode, "span" );

            loadPopoverUI($popover, $(tag).css("color"));

            function isValidSelection(){
                var winSel = window.getSelection();
                return winSel && winSel.type && winSel.type.toUpperCase() == "RANGE";
            }

            function removeReceiveDataListener(handler) {
                if (window.removeEventListener) {
                    window.removeEventListener("message", handler);
                } else if (window.detachEvent) {
                    window.detachEvent("onmessage", handler);
                }
            }

            function registerReceiveDataListener(handler) {
                if (window.addEventListener) {
                    window.addEventListener("message", handler, false);
                } else if (window.attachEvent) {
                    window.attachEvent("onmessage", handler);
                }
            }

            function receiveMessage(event) {
                if (_.isEmpty(event.data)) {
                    return;
                }

                var message = JSON.parse(event.data),
                    action;

                if (!message || message.sender !== GROUP) {
                    return;
                }

                action = message.action;

                if (action === "submit") {
                    if (!_.isEmpty(message.data)) {
                        ek.relayCmd(id, message.data);
                    }
                }else if(action === "remove"){
                    ek.relayCmd(id);
                }

                dialog.hide();

                removeReceiveDataListener(receiveMessage);
            }

            function loadPopoverUI($popover, color) {
                var url = DIALOG_URL + ".html?" + REQUESTER + "=" + GROUP;

                if(!_.isEmpty(color)){
                    url = url + "&" + PICKER_NAME_IN_POPOVER + "=" + color;
                }

                $popover.parent().css("width", ".1px").height(".1px").css("border", "none");
                $popover.css("width", ".1px").height(".1px");
                $popover.find("iframe").attr("src", url);

                //receive the dialog values from child window
                registerReceiveDataListener(receiveMessage);
            }
        },

        //to mark the icon selected/deselected
        updateState: function (selDef) {
            var hasUC = this.editorKernel.queryState(COLOR_PICKER_FEATURE, selDef);

            if (this.pickerUI !== null) {
                this.pickerUI.setSelected(hasUC);
            }
        }
    });

    CUI.rte.plugins.PluginRegistry.register(GROUP, EAEMColorPickerPlugin);

    EAEMColorPickerCmd = new Class({
        toString: "ColorPickerDialogCmd",

        extend: CUI.rte.commands.Command,

        isCommand: function (cmdStr) {
            return (cmdStr.toLowerCase() === COLOR_PICKER_FEATURE);
        },

        getProcessingOptions: function () {
            var cmd = CUI.rte.commands.Command;
            return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST
        },

        _getTagObject: function(color) {
            return {
                "tag": "span",
                "attributes": {
                    "style" : "color: " + color
                }
            };
        },

        execute: function (execDef) {
            var color = execDef.value ? execDef.value[PICKER_NAME_IN_POPOVER] : undefined,
                selection = execDef.selection,
                nodeList = execDef.nodeList;

            if (!selection || !nodeList) {
                return;
            }

            var common = CUI.rte.Common,
                context = execDef.editContext,
                tagObj = this._getTagObject(color);

            //if no color value passed, assume delete and remove color
            if(_.isEmpty(color)){
                nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
                return;
            }

            var tags = common.getTagInPath(context, selection.startNode, tagObj.tag);

            //remove existing color before adding new color
            if (tags != null) {
                nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
            }

            nodeList.surround(execDef.editContext, tagObj.tag, tagObj.attributes);
        }
    });

    CUI.rte.commands.CommandRegistry.register(COLOR_PICKER_FEATURE, EAEMColorPickerCmd);

    //returns the picker dialog html
    //Handlebars doesn't do anything useful here, but the framework expects a template
    function dlgTemplate() {
        CUI.rte.Templates["dlg-" + COLOR_PICKER_DIALOG] =
            Handlebars.compile('<div data-rte-dialog="' + COLOR_PICKER_DIALOG +
                '" class="coral--dark coral-Popover coral-RichText-dialog">' +
                '<iframe width="525px" height="465px"></iframe>' +
                '</div>');
    }

    dlgTemplate();
})(jQuery, jQuery(document), Handlebars);

(function($, $document){
    var SENDER = "experience-aem",
        REQUESTER = "requester",
        COLOR = "color",
        ADD_COLOR_BUT = "#EAEM_CP_ADD_COLOR",
        REMOVE_COLOR_BUT = "#EAEM_CP_REMOVE_COLOR",
        PICKER_COLORS = location.pathname.replace(".html", "") + "/content/items/column/items/picker/colors.infinity.json",
        HELP_BUTTON_SEL = ".cq-dialog-help",
        CANCEL_BUTTON_SEL = ".cq-dialog-cancel",
        SUBMIT_BUTTON_SEL = ".cq-dialog-submit",
        pickerInstance;

    if(queryParameters()[REQUESTER] !== SENDER ){
        return;
    }

    $document.on("foundation-contentloaded", stylePopoverIframe);

    function queryParameters() {
        var result = {}, param,
            params = document.location.search.split(/\?|\&/);

        params.forEach( function(it) {
            if (_.isEmpty(it)) {
                return;
            }

            param = it.split("=");
            result[param[0]] = param[1];
        });

        return result;
    }

    function stylePopoverIframe(){
        var queryParams = queryParameters();

        if(!_.isEmpty(queryParameters()[COLOR])){
            pickerInstance._setColor(decodeURIComponent(queryParams[COLOR]));
        }

        var $dialog = $(".cq-dialog"),
            $cancel = $dialog.find(CANCEL_BUTTON_SEL),
            $submit = $dialog.find(SUBMIT_BUTTON_SEL),
            $addColor = $dialog.find(ADD_COLOR_BUT),
            $removeColor = $dialog.find(REMOVE_COLOR_BUT);

        $dialog.css("border", "solid 2px");
        $dialog.find(HELP_BUTTON_SEL).hide();
        $document.find(".coral-ColorPicker").closest(".coral-Form-fieldwrapper")
            .css("margin-bottom", "20px");

        $document.off("click", CANCEL_BUTTON_SEL);
        $document.off("click", SUBMIT_BUTTON_SEL);
        $document.off("submit");

        $cancel.click(sendCloseMessage);
        $submit.click(sendDataMessage);
        $addColor.click(sendDataMessage);
        $removeColor.click(sendRemoveMessage);
    }

    function sendCloseMessage(){
        var message = {
            sender: SENDER,
            action: "close"
        };

        parent.postMessage(JSON.stringify(message), "*");
    }

    function sendRemoveMessage(){
        var message = {
            sender: SENDER,
            action: "remove"
        };

        parent.postMessage(JSON.stringify(message), "*");
    }

    function sendDataMessage(){
        var message = {
            sender: SENDER,
            action: "submit",
            data: {}
        }, $dialog, color;

        $dialog = $(".cq-dialog");

        color = $dialog.find("[name='./" + COLOR + "']").val();

        if(color && color.indexOf("rgb") >= 0){
            color = CUI.util.color.RGBAToHex(color);
        }

        message.data[COLOR] = color;

        parent.postMessage(JSON.stringify(message), "*");
    }

    CUI.Colorpicker = new Class({
        toString: "Colorpicker",
        extend: CUI.Colorpicker,

        _readDataFromMarkup: function () {
            this.superClass._readDataFromMarkup.call(this);

            var el = this.$element;

            //extend otb CUI.Colorpicker to workaround the pickerModes bug
            //in granite/ui/components/foundation/form/colorpicker/render.jsp
            //colorpickerJson.put("modes", pickerModes); should have been
            //colorpickerJson.put("pickerModes", pickerModes);
            if (el.data('config').modes) {
                this.options.config.displayModes = el.data('config').modes;
            }

            pickerInstance = this;

            function setColors(data){
                if(_.isEmpty(data)){
                    return;
                }

                var colors = {};

                _.each(data, function(color, key){
                    if(key.indexOf("jcr:") >= 0){
                        return;
                    }

                    colors[color.name] = color.value;
                });

                pickerInstance.options.config.colors = colors;
            }

            $.ajax({ url: PICKER_COLORS, async: false, dataType: 'json' } ).done(setColors);
        }
    });

    CUI.Widget.registry.register("colorpicker", CUI.Colorpicker);
})(jQuery, jQuery(document));

8) To set palette type to Free style or Classic, set the following properties on /apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog/content/items/column/items/picker

                  freestylePaletteType="{Boolean}true"
                  classicPaletteType="{Boolean}true"



9) To enable Color Picker for a component, create node experience-aem under rtePlugins (eg. /libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins/experience-aem) and set property features String[] to colorPicker


25 comments:

  1. How to make it shown on editor toolbar? At previous version there was step:
    "Add any text component with RichText editor and in the rtePlugins path of dialog add touchuicolorpicker node to enable color picker plugin"
    I installed the package but it does not show up at the standard Text component.

    ReplyDelete
    Replies
    1. thanks for reporting, updated the post - To enable Color Picker for a component, create node experience-aem under rtePlugins (eg. /libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins/experience-aem) and set property features String[] to colorPicker

      Delete
  2. Is there a way to add !important to the color?

    ReplyDelete
  3. This is great Sreekanth! Thanks so much for publishing this, saving people a TON of work.

    I did run into a couple bugs in Firefox. Perhaps you can update your solution, or at least people can grab the fixes from my post here.

    Bug 1: The colorpicker popup will not open.

    Fix: In the isValidSelection() funciton, change:
    return winSel && winSel.type && winSel.type.toUpperCase() == "RANGE";
    to:
    return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;

    Bug 2: Selecting a colored word in the middle of a sentence and opening the colorpicker does not have the current color defaulted.

    Fix: In the execute() function, change:
    var selection = CUI.rte.Selection.createProcessingSelection(context),
    tag = CUI.rte.Common.getTagInPath(context, selection.startNode, "span" );
    to:
    var selection = CUI.rte.Selection.createProcessingSelection(context);
    var startNode = selection.startNode;
    if (selection.startOffset == selection.startNode.length && startNode != selection.endNode) {
    startNode = startNode.nextSibling;
    }
    var tag = CUI.rte.Common.getTagInPath(context, startNode, "span");

    ReplyDelete
    Replies
    1. Thank you Brett, ill test and update the post later

      Delete
  4. Hi there, can this color picker be used outside the RTE on a custom component? I want to replace the buggy Granite colorpicker with this one :)

    ReplyDelete
  5. Hi, very good post, i tried to test your component but seems the colorpicker's modes are broken.
    Which version of AEM are you using for this? the colorpicker in my AEM 6.1 doesn't set neither edit mode nor free style mode? do you have any idea why?

    Regards.

    ReplyDelete
  6. EsteBusta,

    The Javascript fix above gives some information about the bug in the 6.0/6.1 AEM colorpicker.

    //in granite/ui/components/foundation/form/colorpicker/render.jsp
    //colorpickerJson.put("modes", pickerModes); should have been
    //colorpickerJson.put("pickerModes", pickerModes);

    If anyone happens to know if I can use this new colorpicker standalone (without RTE), that would be great!

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Thanks a lot David Pfundt for the anwser, i missed that part.

    In order to use this "fixed" colorpicker without the RTE, i think is enough if you extend the colorpicker to a new component and just "rewrite" the render.jsp with the solution.

    ReplyDelete
  9. Fantastic! Is there a way to have this available in the inline toolbar also? So combined inline and in fullscreen editor?
    Thank you!

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. hey Brett Birschbach, even after making the changes you suggested, colorpicker popup not getting opened.

    Getting error: Cannot serve request to /apps/touchui-rte-color-picker-plugin/color-picker-popover/cq:dialog.html in /libs/cq/gui/components/authoring/dialog/dialog.jsp

    Can you please suggests how this issue can be resolved?

    ReplyDelete
    Replies
    1. I am facing the same issue. Were you able to solve it ?

      Delete
  12. This comment has been removed by the author.

    ReplyDelete
  13. This comment has been removed by the author.

    ReplyDelete
  14. Whatever fixes I do, I am not able to see the colorpicker in AEM 6.1 SP1 version.

    ReplyDelete
  15. This comment has been removed by the author.

    ReplyDelete
  16. Hello, it seems that the plugin is not working for IE 11, when I select the text and try to select a color in the panel, the text is immediatly unselected and I can't apply the color.

    ReplyDelete
  17. Agilestorelocator.com is the professional in wordpress plugin and plugin is for WordPress. Here is also options available for the Location finder WordPress.

    ReplyDelete
  18. Does anyone have an update to this for 6.2 I'm not seeing the colorpicker in a text component as well.

    ReplyDelete
  19. nice blog too informative. looking and reading your points its so impressive. doing more blog like this. i really appreciated doing like this.
    School Brading UK

    ReplyDelete
  20. it seems that plugin is not working in IE 11 .Does it work for anyone else in IE 11 ?

    ReplyDelete
  21. I was analyzing the Importance of Online Editing Services before I landed on this page and I have learned a lot about the adobe program and I have the skills to use the adobe program effectively. Thanks for sharing this post with us, I have found it to be very educative and entertaining.

    ReplyDelete
  22. I was wondering if anyone had managed to get this addon working in IE or firefox lately?

    ReplyDelete