AEM 65 - Touch UI RTE (Rich Text Editor) Plugin for Creating Structured Content e.g. Creating Tooltips

Goal


Create a Touch UI RTE (Rich Text Editor) plugin for entering Structured Content, converted into HTML and added in RTE based on required functionality

With the solution discussed in this post, author can enter content in a form opened from RTE, convert into HTML, add as a tooltip for selected text

For demo purposes, dialog of core text component v2 was modified to add the 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#structuredContentModal to uiSettings > cui > dialogFullScreen for showing the ellipsis toolbar icon in full screen



RTE Toolbar Icon




Content Form (for Tooltip) in Full Screen


Tooltip applied to text




Tooltip content in CRX



Solution


1) Login to CRXDE Lite, add nt:folder /apps/eaem-touchui-rte-structured-content

2) To show the tooltip form on plugin icon click, create /apps/eaem-touchui-rte-structured-content/structured-content of type cq:Page (line 19, check the clientlib category eaem.rte.structured.content.plugin)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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="cq:Page">
    <jcr:content
        jcr:mixinTypes="[sling:VanityPath]"
        jcr:primaryType="nt:unstructured"
        jcr:title="Experience AEM Structured Content"
        sling:resourceType="granite/ui/components/coral/foundation/page">
        <head jcr:primaryType="nt:unstructured">
            <favicon
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
            <viewport
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
            <clientlibs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
                categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral,eaem.rte.structured.content.plugin]"/>
        </head>
        <body
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/page/body">
            <items jcr:primaryType="nt:unstructured">
                <form
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/coral/foundation/form"
                    class="foundation-form content-container"
                    maximized="{Boolean}true"
                    style="vertical">
                    <items jcr:primaryType="nt:unstructured">
                        <wizard
                            jcr:primaryType="nt:unstructured"
                            jcr:title="Add tooltip content..."
                            sling:resourceType="granite/ui/components/coral/foundation/wizard">
                            <items jcr:primaryType="nt:unstructured">
                                <tooltip
                                    jcr:primaryType="nt:unstructured"
                                    jcr:title="Tooltip"
                                    sling:resourceType="granite/ui/components/coral/foundation/container"
                                    margin="{Boolean}true">
                                    <items jcr:primaryType="nt:unstructured">
                                        <columns
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                                            margin="{Boolean}true">
                                            <items jcr:primaryType="nt:unstructured">
                                                <column
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                                    <items jcr:primaryType="nt:unstructured">
                                                        <title
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            fieldDescription="Enter Title"
                                                            fieldLabel="Title"
                                                            name="title"/>
                                                        <description
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
                                                            fieldDescription="Enter Description"
                                                            fieldLabel="Description"
                                                            name="description"/>
                                                    </items>
                                                </column>
                                            </items>
                                        </columns>
                                    </items>
                                </tooltip>
                                <parentConfig jcr:primaryType="nt:unstructured">
                                    <prev
                                        granite:class="foundation-wizard-control"
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                                        text="Cancel">
                                        <granite:data
                                            jcr:primaryType="nt:unstructured"
                                            foundation-wizard-control-action="cancel"/>
                                    </prev>
                                    <next
                                        granite:class="foundation-wizard-control"
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/button"
                                        disabled="{Boolean}true"
                                        text="Create tooltip"
                                        type="submit"
                                        variant="primary">
                                        <granite:data
                                            jcr:primaryType="nt:unstructured"
                                            foundation-wizard-control-action="next"/>
                                    </next>
                                </parentConfig>
                            </items>
                        </wizard>
                    </items>
                </form>
            </items>
        </body>
    </jcr:content>
</jcr:root>




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

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

                                    rte-structured-content.css


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

.eaem-rte-structured-dialog {
    width: 80%;
    margin-left: -20%;
    height: 83%;
    margin-top: -20%;
    box-sizing: content-box;
    z-index: 10100;
}

.eaem-rte-structured-dialog > iframe {
    width: 100%;
    height: 100%;
    border: 1px solid #888;
}


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

                                    rte-structured-content.js


7) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/rte-structured-content.js, add the following code. #25 function getHtmlFromContent() converts the form content into tooltip html and adds it for selected text. Logic in this function can be adjusted accordingly, to create HTML from form content and add in RTE

(function($, CUI, $document){
    var GROUP = "experience-aem",
        STRUCTURED_CONTENT_FEATURE = "structuredContentModal",
        TCP_DIALOG = "eaemTouchUIStructuredContentModalDialog",
        CONTENT_IN_DIALOG = "content",
        REQUESTER = "requester",
        CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
        MODAL_URL = "/apps/eaem-touchui-rte-structured-content/structured-content.html",
        $eaemStructuredModal, url = document.location.pathname;

    if( url.indexOf(MODAL_URL) !== 0 ){
        addPluginToDefaultUISettings();

        addDialogTemplate();

        addPlugin();
    }else{
        $document.on("foundation-contentloaded", fillDefaultValues);

        $document.on("click", CANCEL_CSS, sendCancelMessage);

        $document.submit(sendTextAttributes);
    }

    function getHtmlFromContent(selectedText, content){
        var tooltipText = content.title + " : " + content.description;

        return "<span title='" + tooltipText + "' class='eaem-dotted-underline' data-content='" +  JSON.stringify(content) + "'>" +
                    selectedText +
                "</span>";
    }

    function setWidgetValue(form, selector, value){
        Coral.commons.ready(form.querySelector(selector), function (field) {
            field.value = _.isEmpty(value) ? "" : decodeURIComponent(value);
        });
    }

    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 sendTextAttributes(){
        var message = {
            sender: GROUP,
            action: "submit",
            data: {}
        }, $form = $("form"), $field;

        _.each($form.find("[name]"), function(field){
            $field = $(field);
            message.data[$field.attr("name")] = $field.val();
        });

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

    function fillDefaultValues(){
        var queryParams = queryParameters(),
            form = $("form")[0];

        if(_.isEmpty(queryParams[CONTENT_IN_DIALOG])){
            return;
        }

        var content = JSON.parse(decodeURIComponent(queryParams[CONTENT_IN_DIALOG]));

        _.each(content, function(value, key){
            setWidgetValue(form, "[name='" + key + "']", value);
        });
    }

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

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

    function getParent() {
        if (window.opener) {
            return window.opener;
        }

        return parent;
    }

    function closeDialogModal(event){
        event = event.originalEvent || {};

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

        var message, action;

        try{
            message = JSON.parse(event.data);
        }catch(err){
            return;
        }

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

        action = message.action;

        if(action === "submit"){
            var ek = $eaemStructuredModal.eaemModalPlugin.editorKernel,
                tooltipHtml = getHtmlFromContent(window.getSelection().toString(), message.data);

            ek.execCmd('inserthtml', tooltipHtml);

            ek.focus();
        }

        var modal = $eaemStructuredModal.data('modal');
        modal.hide();
        modal.$element.remove();
    }

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

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

    function addDialogTemplate(){
        var url = MODAL_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);
    }

    function addPlugin(){
        var TouchUIStructuredContentModalPlugin = new Class({
            toString: "TouchUIStructuredContentModalPlugin",

            extend: CUI.rte.plugins.Plugin,

            modalUI: null,

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

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

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

                this.modalUI = tbGenerator.createElement(STRUCTURED_CONTENT_FEATURE, this, false, { title: "Add tooltip" });
                tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.modalUI, 10);

                var groupFeature = GROUP + "#" + STRUCTURED_CONTENT_FEATURE;
                tbGenerator.registerIcon(groupFeature, "more");

                $(window).off('message', closeDialogModal).on('message', closeDialogModal);
            },

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

                var context = envOptions.editContext,
                    selection = CUI.rte.Selection.createProcessingSelection(context),
                    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,
                    content = $(tag).data("content");

                this.showDialogModal(getModalIFrameUrl(content));

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

                function getModalIFrameUrl(content){
                    var url = MODAL_URL + "?" + REQUESTER + "=" + GROUP;

                    if(_.isObject(content)){
                        url = url + "&" + CONTENT_IN_DIALOG + "=" + JSON.stringify(content);
                    }

                    return url;
                }
            },

            showDialogModal: function(url){
                var self = this, $iframe = $('<iframe>'),
                    $modal = $('<div>').addClass('eaem-rte-structured-dialog coral-Modal');

                $iframe.attr('src', url).appendTo($modal);

                $modal.appendTo('body').modal({
                    type: 'default',
                    buttons: [],
                    visible: true
                });

                $eaemStructuredModal = $modal;

                $eaemStructuredModal.eaemModalPlugin = self;

                $modal.nextAll(".coral-Modal-backdrop").addClass("cfm-coral2-backdrop");
            },

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

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

        CUI.rte.plugins.PluginRegistry.register(GROUP,TouchUIStructuredContentModalPlugin);
    }
}(jQuery, window.CUI,jQuery(document)));



5 comments:

  1. Thanks for the Plugin. Looks like there is an issue for retaining the values for the tooltip while re-editing the dialog the values aren't displaying and on the node the values are storing but multiple values( meaning whatever we have entered all long got added on to it, it is not removing the previously entered text.)
    Example:
    title="testing : tooltip" class="eaem-dotted-underline">title="qwerty : asdfg" class="eaem-dotted-underline" data-content="{"title":"45678","description":""}">testing

    ReplyDelete
  2. Does this work on core component too?

    ReplyDelete
  3. No.. its not retaining the values, on any edit or modify it stores in jcr as multi values.

    ReplyDelete
  4. I based my custom plugin on this one so i had to fix the editing issue. Basically i reworked the handling of the submit action in closeDialogModal:
    In case someone still needs it here is the fix:

    if(action === "submit"){
    var ek = $eaemStructuredModal.eaemModalPlugin.editorKernel,
    context = ek.getEditContext(),
    selection = CUI.rte.Selection.createProcessingSelection(context),
    startNode = selection.startNode;
    if (startNode.parentNode.nodeName === "SPAN" && startNode.parentNode.className === "eaem-dotted-underline") {
    // editing of existing tooltip in chrome
    startNode.parentNode.setAttribute("title", message.data.title);
    startNode.parentNode.setAttribute("data-content", JSON.stringify(message.data));
    } else {
    if (window.getSelection().toString() === startNode.nextSibling.innerText) {
    // editing of existing tooltip in firefox
    startNode.nextSibling.setAttribute("title", message.data.title);
    startNode.nextSibling.setAttribute("data-content", JSON.stringify(message.data));
    } else {
    // initial setting of tooltip in any browser
    var tooltipHtml = getHtmlFromContent(window.getSelection().toString(), message.data);
    ek.execCmd('inserthtml', tooltipHtml);
    }
    }
    ek.focus();
    }

    ReplyDelete
  5. This works very well. Can't we use jQuery instead lodash ?

    ReplyDelete