AEM 62 - Touch UI Composite Multifield with Rich Text Editor (RTE) Required Validator

Goal


Create a composite multifield comprised of rich text editors (widgets of type  cq/gui/components/authoring/dialog/richtext) with the editors set as required in multifield

For AEM 61 (no validator)  check this post

Demo | Package Install


Composite Multifield



RTE Set as Required



Error when Empty



Saved Data in CRX



Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-rte-multifield/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

3) Create file ( type nt:file ) /apps/touchui-rte-multifield/clientlib/js.txt, add the following

                         rte-multifield.js

4) Create file ( type nt:file ) /apps/touchui-rte-multifield/clientlib/rte-multifield.js, add the following code

(function ($, $document) {
    var DATA_EAEM_NESTED = "data-eaem-nested",
        CFFW = ".coral-Form-fieldwrapper",
        RTE_CONTAINER = "richtext-container",
        RTE_EDITABLE = ".coral-RichText-editable",
        FIELD_ERROR_EL = $("<span class='coral-Form-fielderror coral-Icon coral-Icon--alert coral-Icon--sizeS' " +
                                "data-init='quicktip' data-quicktip-type='error' />");

    function addValidator($multifield){
        var $rteContainer, requiredField;

        _.each($multifield.find("." + RTE_CONTAINER), function(rteContainer){
            $rteContainer = $(rteContainer);

            if(invisibleFieldAdded($rteContainer)){
                return;
            }

            requiredField = $rteContainer.find("[aria-required='true']");

            if(_.isEmpty(requiredField)){
                return;
            }

            $rteContainer.children(RTE_EDITABLE).css("height", "5rem");

            //coral validation framework ignores hidden and contenteditable fields, so add an invisible text field
            //the text field is just for registering a validator
            $rteContainer.append("<input type=text style='display:none' value='"
                            + $rteContainer.find(RTE_EDITABLE).text() + "'" +
                            "data-eaem-invisible='true' aria-required='true'/>");

            $rteContainer.children().on("input", function() {
                var $invisibleText = $(this).nextAll("input:text").val($(this).text().trim());

                $invisibleText.checkValidity();
                $invisibleText.updateErrorUI();
            })
        });
    }

    function invisibleFieldAdded($rteContainer){
        return !_.isEmpty($rteContainer.find("[data-eaem-invisible='true']"));
    }

    function validateSubmittables(){
        var $submittables = $("[" + DATA_EAEM_NESTED + "]").find(":-foundation-submittable");

        return Array.prototype.every.call($submittables, function(submittable) {
            var api = $(submittable).adaptTo("foundation-validation");

            return api.checkValidity({
                suppressEvent: true
            });
        });
    }

    function setSelect($field, value){
        var select = $field.closest(".coral-Select").data("select");

        if(select){
            select.setValue(value);
        }
    }

    function setHiddenOrRichText($field, value){
        $field.val(value);

        var $rteContainer = $field.parent();

        if(!$rteContainer.hasClass(RTE_CONTAINER)){
            return;
        }

        $rteContainer.children(RTE_EDITABLE).empty().append(value);
    }

    function setCheckBox($field, value){
        $field.prop( "checked", $field.attr("value") == value);
    }

    function getMultiFieldNames($multifields){
        var mNames = {}, mName;

        $multifields.each(function (i, multifield) {
            mName = $(multifield).children("[name$='@Delete']").attr("name");

            mName = mName.substring(0, mName.indexOf("@"));

            mName = mName.substring(2);

            mNames[mName] = $(multifield);
        });

        return mNames;
    }

    function buildMultiField(data, $multifield, mName){
        $multifield.find(".js-coral-Multifield-add").click(function(){
            var $multifield = $(this).parent();

            setTimeout(function(){
                addValidator($multifield);
            }, 500);
        });

        if(_.isEmpty(mName) || _.isEmpty(data)){
            return;
        }

        _.each(data, function(value, key){
            if(key == "jcr:primaryType"){
                return;
            }

            $multifield.find(".js-coral-Multifield-add").click();

            _.each(value, function(fValue, fKey){
                if(fKey == "jcr:primaryType"){
                    return;
                }

                var $field = $multifield.find("[name='./" + fKey + "']").last(),
                    type = $field.prop("type");

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

                if(type == "select-one"){
                    setSelect($field, fValue);
                }else if(type == "checkbox"){
                    setCheckBox($field, fValue);
                }else if(type == "hidden"){
                    setHiddenOrRichText($field, fValue);
                }else{
                    $field.val(fValue);
                }
            });
        });
    }

    //reads multifield data from server, creates the nested composite multifields and fills them
    function addDataInFields() {
        $(document).on("dialog-ready", readyHandler);

        function readyHandler(){
            var $multifields = $("[" + DATA_EAEM_NESTED + "]");

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

            var mNames = getMultiFieldNames($multifields),
                $form = $(".cq-dialog"),
                actionUrl = $form.attr("action") + ".infinity.json";

            $.ajax(actionUrl).done(postProcess);

            function postProcess(data){
                _.each(mNames, function($multifield, mName){
                    buildMultiField(data[mName], $multifield, mName);
                });
            }
        }
    }

    function fillValue($form, fieldSetName, $field, counter){
        var name = $field.attr("name");

        if (!name) {
            return;
        }

        //strip ./
        if (name.indexOf("./") == 0) {
            name = name.substring(2);
        }

        var value = $field.val();

        if( $field.prop("type") == "checkbox" ){
            value = $field.prop("checked") ? $field.val() : "";
        }

        $('<input />').attr('type', 'hidden')
            .attr('name', fieldSetName + "/" + counter + "/" + name)
            .attr('value', value )
            .appendTo($form);

        //remove the field, so that individual values are not POSTed
        $field.remove();
    }

    //collect data from widgets in multifield and POST them to CRX
    function collectDataFromFields(){
        $(document).on("click", ".cq-dialog-submit", submitHandler);

        function submitHandler(){
            var $multifields = $("[" + DATA_EAEM_NESTED + "]");

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

            var $form = $(this).closest("form.foundation-form"),
                $fieldSets, $fields;

            if(!validateSubmittables()){
                return;
            }

            $multifields.each(function(i, multifield){
                $fieldSets = $(multifield).find("[class='coral-Form-fieldset']");

                $fieldSets.each(function (counter, fieldSet) {
                    $fields = $(fieldSet).children().children(CFFW);

                    $fields.each(function (j, field) {
                        fillValue($form, $(fieldSet).data("name"), $(field).find("[name]"), (counter + 1));
                    });
                });
            });
        }
    }

    $.validator.register({
        selector: "[data-eaem-invisible='true']",

        validate: function ($invisibleText) {
            var cuiRichText = $invisibleText.prevAll(RTE_EDITABLE).data("richText");

            if(!cuiRichText || !cuiRichText.editorKernel){
                return;
            }

            var isRequired = ($invisibleText.attr("aria-required") === "true");

            if (isRequired && _.isEmpty(cuiRichText.editorKernel.getProcessedHtml())) {
                return "Please fill this field";
            }

            return null;
        },

        show: function ($invisibleText, message) {
            var $field = $invisibleText.prevAll(RTE_EDITABLE),
                arrow = $invisibleText.closest("form").hasClass("coral-Form--vertical") ? "right" : "top";

            FIELD_ERROR_EL.clone()
                .attr("data-quicktip-arrow", arrow)
                .attr("data-quicktip-content", message)
                .insertAfter($field);

            $field.attr("aria-invalid", "true").toggleClass("is-invalid", true);
        },

        clear: function ($invisibleText) {
            var $field = $invisibleText.prevAll(RTE_EDITABLE);

            $field.removeAttr("aria-invalid").removeClass("is-invalid")
                .nextAll(".coral-Form-fielderror").tooltip("hide").remove();
        }
    });

    $document.ready(function () {
        addDataInFields();
        collectDataFromFields();
    });
}(jQuery, jQuery(document)));

4 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Uploaded the package and tested the package.
    When you open up the dialog the first pointer comes to rte text editor and when you just click somewhere else the rte text is not available for editing later on.
    And when you open up the maximized view of the dialog, then also it does not allow the editing inside it.

    ReplyDelete
  3. Can you please share this package here itself instead of giving google drive as it is blocked in our organisation

    ReplyDelete
  4. We were using 6.1 multifield js now we are facing issues with that in aem 6.2 for rte in multifield. Is there a way to fix that?

    ReplyDelete