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

Goal


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

For a similar extension on 6.5.17 with custom colors from data source Check Github | 64 check this post | 63 check this post | 62 check this post | 61 check this post

In Core Components 2.8.0 uiSettings/cui/dialogFullScreen was removed and the plugin settings were moved to Template Policies, for making the RTE Color plugin work with Core Components 2.8.0 check this post

For demo purposes, dialog of core text component v2 was modified to add the color picker configuration - /apps/core/wcm/components/text/v2/text/cq:dialog/content/items/tabs/items/properties/items/columns/items/column/items/text/rtePlugins

Demo | Package Install | Github


Plugin Configuration

                                         Add node experience-aem under rtePlugins and set features=* 



                                         Add experience-aem#colorPicker to uiSettings > cui > dialogFullScreen for showing the color palette toolbar icon in full screen




RTE Toolbar Icon




Color Picker in Full Screen


Color applied to text




Text with Color in CRX



Solution


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

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

<?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/coral/foundation/form/colorfield"
                        name="./color">
                        <items 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"/>
                        </items>
                    </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>


                           
3) Create clientlib (cq:ClientLibraryFolder) /apps/eaem-touchui-dialog-rte-color-picker/clientlib set property categories to cq.authoring.dialog.all and dependencies to [underscore]

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

                                    color-picker.js


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

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

    if( url.indexOf(PICKER_URL) !== 0 ){
        addPluginToDefaultUISettings();
        addDialogTemplate();
    }

    var EAEMColorPickerDialog = new Class({
        extend: CUI.rte.ui.cui.AbstractDialog,

        toString: "EAEMColorPickerDialog",

        initialize: function(config) {
            this.exec = config.execute;
        },

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

    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, false, { title: "Color Picker" });
            tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 10);

            var groupFeature = GROUP + "#" + COLOR_PICKER_FEATURE;
            tbGenerator.registerIcon(groupFeature, "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"), plugin = this, dialog,
                color = $(tag).css("color"),
                dm = ek.getDialogManager(),
                $container = CUI.rte.UIUtils.getUIContainer($(context.root)),
                propConfig = {
                    'parameters': {
                        'command': this.pluginId + '#' + COLOR_PICKER_FEATURE
                    }
                };

            if(this.eaemColorPickerDialog){
                dialog = this.eaemColorPickerDialog;
            }else{
                dialog = new EAEMColorPickerDialog();

                dialog.attach(propConfig, $container, this.editorKernel);

                dialog.$dialog.css("-webkit-transform", "scale(0.9)").css("-webkit-transform-origin", "0 0")
                    .css("-moz-transform", "scale(0.9)").css("-moz-transform-origin", "0px 0px");

                dialog.$dialog.find("iframe").attr("src", getPickerIFrameUrl(color));

                this.eaemColorPickerDialog = dialog;
            }

            dm.show(dialog);

            registerReceiveDataListener(receiveMessage);

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

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

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

                return url;
            }

            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);
                    plugin.eaemColorPickerDialog = null;
                }else if(action === "cancel"){
                    plugin.eaemColorPickerDialog = null;
                }

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

    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.commonAncestor = nodeList.nodes[0].dom.parentNode;
            }

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

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

    function addPluginToDefaultUISettings(){
        var toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.inline.toolbar;
        toolbar.splice(3, 0, GROUP + "#" + COLOR_PICKER_FEATURE);

        toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.fullscreen.toolbar;
        toolbar.splice(3, 0, GROUP + "#" + COLOR_PICKER_FEATURE);
    }

    function addDialogTemplate(){
        var url = PICKER_URL + "?" + REQUESTER + "=" + GROUP;

        var html = "<iframe width='600px' height='500px' frameBorder='0' src='" + url + "'></iframe>";

        if(_.isUndefined(CUI.rte.Templates)){
            CUI.rte.Templates = {};
        }

        if(_.isUndefined(CUI.rte.templates)){
            CUI.rte.templates = {};
        }

        CUI.rte.templates['dlg-' + TCP_DIALOG] = CUI.rte.Templates['dlg-' + TCP_DIALOG] = Handlebars.compile(html);
    }
}(jQuery, window.CUI,jQuery(document)));

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

    $(function(){
        _.defer(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(),
            $dialog = $("coral-dialog");

        if(_.isEmpty($dialog)){
            return;
        }

        $dialog.css("overflow", "hidden").css("background-color", "#fff");

        $dialog[0].open = true;

        var $addColor = $dialog.find(ADD_COLOR_BUT),
            $removeColor = $dialog.find(REMOVE_COLOR_BUT),
            color = queryParameters()[COLOR],
            $colorPicker = $document.find("coral-colorinput");

        if(!_.isEmpty(color)){
            color = decodeURIComponent(color);

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

            $colorPicker[0].value = color;
        }

        adjustHeader($dialog);

        $colorPicker.css("margin-bottom", "285px");

        $(ADD_COLOR_BUT).css("margin-left", "220px");

        $addColor.click(sendDataMessage);

        $removeColor.click(sendRemoveMessage);
    }

    function adjustHeader($dialog){
        var $header = $dialog.css("background-color", "#fff").find(".coral3-Dialog-header");

        $header.find(".cq-dialog-submit").remove();

        $header.find(".cq-dialog-cancel").click(function(event){
            event.preventDefault();

            $dialog.remove();

            sendCancelMessage();
        });
    }

    function sendCancelMessage(){
        var message = {
            sender: SENDER,
            action: "cancel"
        };

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


8 comments:

  1. We are having the an issue on 6.5 of Uncaught TypeError: Handlebars.compile is not a function. Is there an include or something we're missing?

    ReplyDelete
    Replies
    1. We had to add "handlebars" as a dependency in the clientlib, in addition to underscore.

      Delete
    2. Hi Metamort, did you change anything else? I am using 6.5 and tried to add Hadlebars and underscore but still not working for me.

      Delete
  2. Hi,
    Thanks for the document provided. it is working fine in aem 6.4 but the only problem is,
    It is storing as RGB(100,1,1) values if we change color. is it possible to change this.
    Thanks

    ReplyDelete
  3. Hi Sreekanth,

    Thank you for writing this article. Please can you advise me if the this works on a clean installation of AEM 6.5.2.0 with "We-Retail" sample content ?

    I have tried following the steps below and I cannot see the Color Picker icon in the Rich Text Editor ?

    (1) Navigate to http://localhost:4502/crx/de/index.jsp#/apps/core/wcm/components/text/v2/text/cq%3Adialog/content/items/tabs/items/properties/items/columns/items/column/items/text/rtePlugins
    (2) Create node "experience-aem"
    (3) Navigate to http://localhost:4502/crx/de/index.jsp#/apps/core/wcm/components/text/v2/text/cq%3Adialog/content/items/tabs/items/properties/items/columns/items/column/items/text/rtePlugins/experience-aem
    (4) Add property "features" with value "*"
    (5) Navigate to http://localhost:4502/crx/de/index.jsp#/apps/core/wcm/components/text/v2/text/cq%3Adialog/content/items/tabs/items/properties/items/columns/items/column/items/text/uiSettings/cui/dialogFullScreen
    (6) Add value "experience-aem#colorPicker" to existing property "toolbar"
    (7) Navigate to http://localhost:4502/crx/packmgr/index.jsp
    (8) Upload and install package "eaem-touchui-dialog-rte-color-picker-65.zip"
    (9) Navigate to http://localhost:4502/crx/de/index.jsp#/apps/eaem-touchui-dialog-rte-color-picker
    (10) Verify this contains folders "clientlib" and "color-picker-popover"
    (11) Navigate to http://localhost:4502/editor.html/content/we-retail/language-masters/en.html
    (12) Add a Text component
    (13) Select Text component , click "Edit" and "Full screen"

    Please can you advise me if there are any additional steps needed to get the Rich Text Editor Color Picker working on a clean installation of AEM 6.5.2.0 with "We-Retail" sample content ?

    Thank you for your time and advise,
    Best regards,
    James Collett

    ReplyDelete
  4. Hi Sreekanth

    Thanks for this plugin . We are using this in our project. The issue we are having is in the XF pages, the plugin is throwing Handlebars.compile is not a function error. I tried adding handlebars as additional dependencies value , but it didn't solve the issue. Any idea ?

    TIA
    Veena

    ReplyDelete
  5. Hello,

    I insterded this plugin in my project, it works fine in dialog and fullscreen editors, but it doesn't work on inline editor.
    I don't know if it is only my problem or the plugin itself. Is this plugin supposed to work even in the inline editor or only in fullscreen and dialog?

    Thanks in advance.

    ReplyDelete
  6. Hi Srikanth,

    I have added this plugin to inPlaceEditing node also. I'm able to see the color picker plugin only for first time, it disappears from inplace editing mode once i switch to dialog mode.

    Please provide your thoughts.

    ReplyDelete