AEM 62 - Touch UI Dialog RTE (Rich Text Editor) Color Picker Plugin

Goal


Touch UI Color Picker Plugin for Dialog RTE (Rich Text Editor) - /libs/cq/gui/components/authoring/dialog/richtext

For a similar extension (Inplace editing) on 63 check this post 61 check this post

For demo purposes, dialog of foundation text component was modified to add the color picker configuration - /libs/foundation/components/text/cq:dialog/content/items/tabs/items/text/items/column/items/text/rtePlugins

Thank you Brett Birschbach for the firefox bug fix

Demo on Chrome | Demo on Firefox | Package Install


Plugin Configuration



Add Plugin to RTE Toolbar



Picker with Free Style Palette - Inline Dialog



Picker with Free Style Palette - Full Screen Dialog



Picker with Palette Shades - Classic



Palette Edit Mode



RTE text with Color




Solution


1) Login to CRXDE Lite, add nt:folder /apps/touchui-dialog-mini-rte-color-picker

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




3) XML representation of /apps/touchui-dialog-mini-rte-color-picker/color-picker-popover/cq:dialog

<?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="/apps/touchui-dialog-mini-rte-color-picker/color-picker"
                        editType="{Boolean}true"
                        freestylePaletteType="{Boolean}true"
                        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-dialog-mini-rte-color-picker/color-picker-popover/cq:dialog/content/items/column/items/picker/colors

5) To workaround the pickerModes bug register a new color picker widget /apps/touchui-dialog-mini-rte-color-picker/color-picker/render.jsp extending ootb color picker widget /libs/granite/ui/components/foundation/form/colorpicker, add the following code

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%
    Config mCfg = cmp.getConfig();

    String COLOR_PICKER_WRAPPER_ID = "eaem-color-picker-wrapper-" + mCfg.get("name", String.class).substring(2);
%>

<div id="<%=COLOR_PICKER_WRAPPER_ID%>">
    <%--include ootb color picker--%>
    <sling:include resourceType="/libs/granite/ui/components/foundation/form/colorpicker"/>
</div>

<script>
    (function($){
        var wrapper = $("#<%=COLOR_PICKER_WRAPPER_ID%>"),
                colorPicker = wrapper.find("[data-init='colorpicker']");

        if(_.isEmpty(colorPicker)){
            console.log("EAEM - color picker wrapper not found");
            return;
        }

        //extend otb 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);
        var config = colorPicker.data("config");
        config.pickerModes = config.modes;

        delete config.modes;

        colorPicker.attr("data-config", config);
    }(jQuery));
</script>

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

7) Create file (nt:file) /apps/touchui-dialog-mini-rte-color-picker/clientlib/js.txt, add the following content

                   color-picker.js

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

(function($, CUI){
    var GROUP = "experience-aem",
        COLOR_PICKER_FEATURE = "colorPicker",
        COLOR_PICKER_MODAL_DIV = "eaem-color-picker",
        PICKER_NAME_IN_POPOVER = "color",
        REQUESTER = "requester",
        PICKER_URL = "/apps/touchui-dialog-mini-rte-color-picker/color-picker-popover/cq:dialog.html";

    var TouchUIColorPickerPlugin = new Class({
        toString: "TouchUIColorPickerPlugin",

        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, "Color Picker");
            tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 120);

            var groupFeature = GROUP + "#" + COLOR_PICKER_FEATURE;
            tbGenerator.registerIcon(groupFeature, "coral-Icon coral-Icon--textColor");
        },

        execute: function (id, value, envOptions) {
            if(!isValidSelection()){
                return;
            }

            var context = envOptions.editContext,
                selection = CUI.rte.Selection.createProcessingSelection(context),
                ek = this.editorKernel,
                startNode = selection.startNode;

            if ( (selection.startOffset === startNode.length) && (startNode != selection.endNode)) {
                startNode = startNode.nextSibling;
            }

            var tag = CUI.rte.Common.getTagInPath(context, startNode, "span"),
                content = this.getPickerIFrameContent($(tag).css("color"));

            this.addModalDiv();

            var modal = new CUI.Modal({
                element : '#' + COLOR_PICKER_MODAL_DIV,
                heading : "Pick a Color",
                content: content
            });

            registerReceiveDataListener(receiveMessage);

            function isValidSelection(){
                var winSel = window.getSelection();
                return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;
            }

            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);
                }

                modal.hide();

                removeReceiveDataListener(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);
            }
        },

        getModalHtml: function(){
            return "<div class=\"coral-Modal-header\">"
                        + "<h2 class=\"coral-Modal-title coral-Heading coral-Heading--2\"></h2>"
                        + "<i class=\"coral-Modal-typeIcon coral-Icon coral-Icon--sizeS\"></i>"
                        + "<button type=\"button\" "
                            + "class=\"coral-MinimalButton coral-Modal-closeButton\" "
                            + "data-dismiss=\"modal\">"
                        + "<i class=\"coral-Icon coral-Icon--sizeXS coral-Icon--close "
                                + "coral-MinimalButton-icon\"></i>" + "</button>"
                    + "</div>"
                    + "<div class=\"coral-Modal-body legacy-margins\"></div>";
        },

        addModalDiv: function(){
            var $modalDiv = $("#" + COLOR_PICKER_MODAL_DIV);

            if (!_.isEmpty($modalDiv) ) {
                return;
            }

            var tag = {
                class: "coral-Modal",
                id: COLOR_PICKER_MODAL_DIV
            };

            var insertModal = $("<div>", tag).hide().html(this.getModalHtml());

            $(document.body).append(insertModal);
        },

        getPickerIFrameContent: function(color){
            var url = PICKER_URL + "?" + REQUESTER + "=" + GROUP;

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

            return "<iframe width='520px' height='405px' frameBorder='0' src='" + url + "'></iframe>";
        }
    });

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

    var TouchUIColorPickerCmd = new Class({
        toString: "TouchUIColorPickerCmd",

        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, TouchUIColorPickerCmd);
}(jQuery, window.CUI));

(function($, $document){
    var SENDER = "experience-aem",
        REQUESTER = "requester",
        COLOR = "color",
        ADD_COLOR_BUT = "#EAEM_CP_ADD_COLOR",
        REMOVE_COLOR_BUT = "#EAEM_CP_REMOVE_COLOR";

    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();

        var $dialog = $(".cq-dialog").css("background", "white"),
            $addColor = $dialog.find(ADD_COLOR_BUT),
            $removeColor = $dialog.find(REMOVE_COLOR_BUT),
            $colorPicker = $document.find(".coral-ColorPicker"),
            pickerInstance = $colorPicker.data("colorpicker");

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

        $dialog.find(".cq-dialog-header").hide();
        $dialog.find(".cq-dialog-content").css("top", ".1rem");
        $colorPicker.closest(".coral-Form-fieldwrapper").css("margin-bottom", "285px");
        $(ADD_COLOR_BUT).css("margin-left", "250px");

        $addColor.click(sendDataMessage);
        $removeColor.click(sendRemoveMessage);
    }

    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), "*");
    }
})(jQuery, jQuery(document));

6 comments:

  1. i have followed the above configuration but still i am not able to see the color picker icon.

    ReplyDelete
    Replies
    1. we has the same issue within our project and found out that the JS is not minify compliant. you can either refactor the js yourself or disable minify for the authoring instance.

      Delete
  2. This blog explains the details of most popular technological details. This helps to learn about what are all the different method is there. And the working methods all of that are explained here. Informative blog.
    School Rebranding UK

    ReplyDelete
  3. When i try to select the text and open the Plugin, the text selection disappears. Because of this, the span with the color gets inserted after the text. Any help?

    ReplyDelete
  4. My JS is not minified. I have followed the instructions properly. But I couldn't still make it work. I'm seeing the browser console throws TypeError: CUI.rte is undefined. Please help

    ReplyDelete
  5. Thank Sreekanth Choudry Nalabotu for this nice plugin.

    Ravi, please state the steps you took to install it. It should work just fine with OOT text component if you follow the steps at the beginning of the page.

    Let me know.
    Regards
    Daniel.

    ReplyDelete