AEM 61 - Touch UI Rich Text Editor (RTE) Browse and Insert Image

Goal


Touch UI Rich Text Editor (RTE) Plugin to open a DialogSelect Image and add it in RTE

Dialog for the plugin can be configured with any standard Touch UI Widgets. In this post, we configure a textfield - /libs/granite/ui/components/foundation/form/textfield for entering alt text and path browser - /libs/granite/ui/components/foundation/form/pathbrowser for selecting image

Demo | Package Install


Component Dialog RTE Config

Add the image insert plugin - touchuiinsertimage, available in group experience-aem in component dialog eg. /libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins (for demonstration only; never modify foundation components)



Plugin Dialog with Path Browser Config



Plugin Dialog



Plugin Dialog with Picker



Plugin Dialog with Image Selected



Image Shown in RTE



Image Source in RTE


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-rte-browse-insert-image

2)  Add the dialog configuration for RTE Plugin, shown in popover window when user clicks on image plugin icon; create node of type sling:Folder /apps/touchui-rte-browse-insert-image/popover with the following configuration

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:Folder"
    jcr:title="Pick an Image"
    sling:resourceType="cq/gui/components/authoring/dialog"/>

3) Add dialog content /apps/touchui-rte-browse-insert-image/popover/content; #5 attribute eaem-rte-iframe-content marks this dialog RTE specific

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/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"
    sling:resourceType="granite/ui/components/foundation/container"
    eaem-rte-iframe-content="">
    <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">
                <alt
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/foundation/form/textfield"
                    fieldLabel="Alt Text"
                    name="./alt"/>
                <image
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                    fieldLabel="Image"
                    name="./image"
                    rootPath="/content/dam"/>
            </items>
        </column>
    </items>
</jcr:root>

4) Dialog in CRXDE Lite



5) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-rte-browse-insert-image/clientlib, set property categories of String type to rte.coralui2

6) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/js.txt, add the following

                         image-insert.js
                         popover.js

7) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/image-insert.js, add the following code. This file contains logic for image plugin; receiving selected image from popover iframe showing the dialog and adding it in RTE

(function ($, $document, Handlebars) {
    var ExperienceAEM = {
        GROUP: "experience-aem",
        TIM_FEATURE: "touchuiinsertimage",
        TIM_DIALOG: "touchuiinsertimagedialog",
        CONTENT_URL: "/apps/touchui-rte-browse-insert-image/popover.html",
        EAEM_RTE_IFRAME_CONTENT: "eaem-rte-iframe-content"
    };

    ExperienceAEM.TIM_UI_SETTING = ExperienceAEM.GROUP + "#" + ExperienceAEM.TIM_FEATURE;

    //extend toolbar builder to register insert image
    ExperienceAEM.CuiToolbarBuilder = new Class({
        toString: "EAEMCuiToolbarBuilder",

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

        _getUISettings: function (options) {
            var uiSettings = this.superClass._getUISettings(options);

            //inline toolbar
            var toolbar = uiSettings["inline"]["toolbar"],
                feature = ExperienceAEM.TIM_UI_SETTING;

            //uncomment this to make image insert available for inline toolbar
            /*if (toolbar.indexOf(feature) == -1) {
                var index = toolbar.indexOf("fullscreen#start");
                toolbar.splice(index, 0, feature);
                toolbar.splice(index + 1, 0, "-");
            }*/

            //add image insert to fullscreen toolbar
            toolbar = uiSettings["fullscreen"]["toolbar"];

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

            if (!this._getClassesForCommand(feature)) {
                this.registerAdditionalClasses(feature, "coral-Icon coral-Icon--image");
            }

            return uiSettings;
        }
    });

    //popover dialog thats hosts iframe
    ExperienceAEM.InsertImageDialog = new Class({
        extend: CUI.rte.ui.cui.AbstractBaseDialog,

        toString: "EAEMInsertImageDialog",

        getDataType: function () {
            return ExperienceAEM.TIM_DIALOG;
        }
    });

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

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

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

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

            var dialog = new ExperienceAEM.InsertImageDialog();
            dialog.attach(config, $container, this.editorKernel, true);

            return dialog;
        }
    });

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

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

        createToolbarBuilder: function () {
            return new ExperienceAEM.CuiToolbarBuilder();
        },

        createDialogManager: function (editorKernel) {
            return new ExperienceAEM.DialogManager(editorKernel);
        }
    });

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

    ExperienceAEM.TouchUIInsertImagePlugin = new Class({
        toString: "TouchUIInsertImagePlugin",

        extend: CUI.rte.plugins.Plugin,

        pickerUI: null,

        getFeatures: function () {
            return [ ExperienceAEM.TIM_FEATURE ];
        },

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

            if (this.isFeatureEnabled(ExperienceAEM.TIM_FEATURE)) {
                this.pickerUI = tbGenerator.createElement(ExperienceAEM.TIM_FEATURE, this, true, "Insert Image");
                tbGenerator.addElement(ExperienceAEM.GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 120);
            }
        },

        execute: function (id) {
            var ek = this.editorKernel,
                dm = ek.getDialogManager();

            var dialogConfig = {
                parameters: {
                    "command": ExperienceAEM.TIM_UI_SETTING
                }
            };

            var dialog = this.dialog = dm.create(ExperienceAEM.TIM_DIALOG, dialogConfig);

            dm.prepareShow(this.dialog);

            dm.show(this.dialog);

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

            loadPopoverUI($popover);

            function loadPopoverUI($popover) {
                $popover.parent().css("width", ".1px").height(".1px").css("border", "none");
                $popover.css("width", ".1px").height(".1px");

                $popover.find("iframe").attr("src", ExperienceAEM.CONTENT_URL);

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

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

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

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

                var action = message.action;

                if(action == "submit"){
                    var data = message.data;

                    if(!_.isEmpty(data) && !_.isEmpty(data.imagePath)){
                        ek.relayCmd(id, message.data);
                    }
                }

                dialog.hide();

                removeReceiveDataListener(receiveMessage);
            }

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

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

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

    CUI.rte.plugins.PluginRegistry.register(ExperienceAEM.GROUP, ExperienceAEM.TouchUIInsertImagePlugin);

    ExperienceAEM.InsertImageCmd = new Class({
        toString: "InsertImageCmd",

        extend: CUI.rte.commands.Command,

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

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

        execute: function (execDef) {
            var data = execDef.value, path = data.imagePath, alt = data.altText || "",
                width = 100, height = 100,
                imageUrl = CUI.rte.Utils.processUrl(path, CUI.rte.Utils.URL_IMAGE),
                imgHtml = "";

            imgHtml += "<img src=\"" + imageUrl + "\" alt=\"" + alt + "\"";
            imgHtml += " " + CUI.rte.Common.SRC_ATTRIB + "=\"" + path + "\"";
            imgHtml += " width=\"" + width + "\"";
            imgHtml += " height=\"" + height + "\"";
            imgHtml += ">";

            execDef.editContext.doc.execCommand("insertHTML", false, imgHtml);
        }
    });

    CUI.rte.commands.CommandRegistry.register(ExperienceAEM.GROUP, ExperienceAEM.InsertImageCmd);

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

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

8) Create file ( type nt:file ) /apps/touchui-rte-browse-insert-image/clientlib/popover.js, add the following code. This file contains logic for sending the dialog values like image selected to parent window RTE

(function($, $document){
    //dialogs marked with eaem-rte-iframe-content data attribute execute the below logic
    //to send dialog values to parent window RTE
    var EAEM_RTE_IFRAME_CONTENT = "eaem-rte-iframe-content",
        HELP_BUTTON_SEL = ".cq-dialog-help",
        CANCEL_BUTTON_SEL = ".cq-dialog-cancel",
        SUBMIT_BUTTON_SEL = ".cq-dialog-submit",
        ALT_TEXT_NAME = "./alt",
        IMAGE_NAME = "./image";

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

    function stylePopoverIframe(){
        var $iframeContent = $("[" + 'data-' + EAEM_RTE_IFRAME_CONTENT + "]");

        if(_.isEmpty($iframeContent)){
            return
        }

        var $form = $iframeContent.closest("form"),
            $cancel = $form.find(CANCEL_BUTTON_SEL),
            $submit = $form.find(SUBMIT_BUTTON_SEL);

        $form.css("border", "solid 2px");
        $form.find(HELP_BUTTON_SEL).hide();

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

        $cancel.click(sendCloseMessage);
        $submit.click(sendDataMessage);
    }

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

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

    function sendDataMessage(){
        var message = {
            sender: EAEM_RTE_IFRAME_CONTENT,
            action: "submit",
            data:{
                altText: $("[name='" + ALT_TEXT_NAME + "']").val(),
                imagePath: $("[name='" + IMAGE_NAME + "']").val()
            }
        };

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

26 comments:

  1. Good one sreekanth & thanks for sharing

    ReplyDelete
  2. Thanks Sreekanth
    very usefull, a feature that is often requested

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

    ReplyDelete
  4. First off, thank you very much Sreekanth for this post! This is a feature that our users have wanted since we started on our project. I am currently trying to implement both this and the color picker but I'm having issues with that. I am only able to get this to work when I purposely break the color-picker-plugin.js. If I do that then the image browser icon shows up in the toolbar for the full screen rich text editor. Otherwise it does not show up at all. Were you able to get both of these to work together?

    ReplyDelete
    Replies
    1. Brendan, thanks for reporting; i think because of

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

      the last one loading wins (overrrides previous register); ill work on a fix

      Delete
    2. Hello Sreekanth,

      Just curious if you've had an opportunity to look at this at all? I did some digging of my own and found the ToolKitRegistry.js file but don't have enough understanding of it to correct this issue.

      Delete
    3. Brendan,

      check the following demo (shows configuration too)

      https://drive.google.com/file/d/0B4d6KmbLkAumZk5JV0ZZWHBFenc/view?usp=sharing

      the fix is not robust enough, not finding enough time... but if you'd like to try it out

      https://drive.google.com/file/d/0B4d6KmbLkAumN1VDXzVqRUJ1TVU/view?usp=sharing

      Delete
    4. Thanks again Sreekanth,

      Just curious if you plan on revisiting this at all? If not could you please be more specific on what specifically is not robust enough about the fix?

      Delete
    5. Thanks again Sreekanth,

      Just curious if you plan on revisiting this at all? If not could you please be more specific on what specifically is not robust enough about the fix?

      Delete
  5. Sreekanth,

    I was able to get the colorpicker and image insert to work together by making the following changes:

    - Remove the custom EAEMCuiToolbarBuilder and EAEMDialogManager classes, instead extending CUI.rte.ui.cui.CuiToolbarBuilder and CUI.rte.ui.cui.CuiDialogManager directly - e.g. CUI.rte.ui.cui.CuiDialogManager = new Class({ extend: CUI.rte.ui.cui.CuiDialogManager, ... });

    - Remove the EAEMToolkitImpl class and the line `CUI.rte.ui.ToolkitRegistry.register("cui", EAEMToolkitImpl);` as it seems no longer necessary

    - Place the colorpicker and image insert plugins into different GROUP names, so that they dont collide when `CUI.rte.plugins.PluginRegistry.register(GROUP, EAEMColorPickerPlugin);` is called.

    ReplyDelete
    Replies
    1. Looks like I then also needed to change calls of `this.superClass._getUISettings(options)` and `this.superClass.create.call(this, dialogId, config)` to `this.inherited(arguments)`.

      Delete
  6. Sreekanth,

    In Chrome, the inserthtml command is adding garbage styling into the img tag for me, and also adding a span with font size and color around the text after the inserted image.

    To work around that, I replaced
    execDef.editContext.doc.execCommand("insertHTML", false, imgHtml);
    with
    var range = execDef.editContext.win.getSelection().getRangeAt(0);
    range.deleteContents();

    var el = execDef.editContext.doc.createElement("div");
    el.innerHTML = imgHtml;
    range.insertNode(el.firstChild);

    ReplyDelete
  7. Also, one other bug fix.

    I added `dialog.restoreSelectionOnHide = false;` because otherwise after adding an image the cursor position would be saved in the old location, and if I tried using any of the other RTE tools it would move my cursor from the current position back to the position of the image.

    ReplyDelete
    Replies
    1. Brett / sreekanth,
      `dialog.restoreSelectionOnHide = false;` is not working as expected and still cursor is moving from the current position back to the position of the image.

      Delete
    2. Hi Brett,

      Where in the code should 'dialog.restoreSelectionOnHide = false;' be added?

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

      Delete
  8. Hi Sreekanth,
    I have installed this package and did the node config as per the demo, but I could not see the image button on my RTE touch UI, could please let me know if I miss anything. Thanks in advance.

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

    ReplyDelete
  10. Hey Sreekanth, Thank you for the post. i have a question for you , is there a way that i could collect all the image paths and store them at the text node along with the "text" property, say another property containing the image paths?

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

    ReplyDelete
  12. Hi, Can anyone share me the popover.html file ,which is being used in the js file ?

    CONTENT_URL: "/apps/touchui-rte-browse-insert-image/popover.html

    Thanks

    ReplyDelete
  13. Hi - we're using AEM 6.2 and I keep getting this error after selecting the image in the "pick an image" dialogue window.
    _is not defined.
    It appears to be coming from the popover.js file:
    if(_.isEmpty($iframeContent)){
    return;

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  14. Hi Sreekanth,
    Thanks for your solution which is the requirement of every client in almost all the projects for an RTE. As per the above comments I see there are few fixes made and few issues with 6.2, May I use this now directly on 6.2 version of AEM or any changes to be made for the above files and package you've given ?

    Thanks a lot for your work and sharing it with the community.

    ReplyDelete
  15. It does not work on 6.3. Someone who has succeeded can help me

    ReplyDelete