AEM 62 - Touch UI Composite Image Multifield

Goal


Create  a Touch UI Composite Multifield configuration supporting Images, widgets of type granite/ui/components/foundation/form/fileupload

For Classic UI Image Multifield check this post

For AEM 61 Touch UI Image Multifield check this post

Demo | Package Install


Component Rendering




Image Multifield Structure




Nodes in CRX




Dialog




Dialog XML

<?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="62 Touch UI Image Multi Field"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/tabs"
            type="nav"/>
        <items jcr:primaryType="nt:unstructured">
            <company
                jcr:primaryType="nt:unstructured"
                jcr:title="Company"
                sling:resourceType="granite/ui/components/foundation/section">
                <layout
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                <items jcr:primaryType="nt:unstructured">
                    <column
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/foundation/container">
                        <items jcr:primaryType="nt:unstructured">
                            <fieldset
                                jcr:primaryType="nt:unstructured"
                                jcr:title="Products"
                                sling:resourceType="granite/ui/components/foundation/form/fieldset">
                                <layout
                                    jcr:primaryType="nt:unstructured"
                                    sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                                <items jcr:primaryType="nt:unstructured">
                                    <column
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/container">
                                        <items jcr:primaryType="nt:unstructured">
                                            <company
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                fieldDescription="Enter Company Name"
                                                fieldLabel="Company"
                                                name="./company"/>
                                            <product
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/multifield"
                                                class="full-width"
                                                eaem-nested=""
                                                fieldDescription="Click '+' to add a Product"
                                                fieldLabel="Product">
                                                <field
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                                    name="./products">
                                                    <layout
                                                        jcr:primaryType="nt:unstructured"
                                                        sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                                                        method="absolute"/>
                                                    <items jcr:primaryType="nt:unstructured">
                                                        <column
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/container">
                                                            <items jcr:primaryType="nt:unstructured">
                                                                <productName
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                                    fieldDescription="Enter Product Name"
                                                                    fieldLabel="Product"
                                                                    name="./productName"/>
                                                                <language
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/select"
                                                                    class="language"
                                                                    fieldLabel="Language"
                                                                    name="./language">
                                                                    <datasource
                                                                        jcr:primaryType="nt:unstructured"
                                                                        sling:resourceType="cq/gui/components/common/datasources/languages"
                                                                        addNone="{Boolean}true"/>
                                                                </language>
                                                                <show
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/checkbox"
                                                                    name="./show"
                                                                    text="Show"
                                                                    value="yes"/>
                                                                <productImage
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/fileupload"
                                                                    autoStart="{Boolean}false"
                                                                    class="cq-droptarget"
                                                                    fieldLabel="Product"
                                                                    fileNameParameter="./productImageName"
                                                                    fileReferenceParameter="./productImageRef"
                                                                    mimeTypes="[image]"
                                                                    multiple="{Boolean}false"
                                                                    name="./productImage"
                                                                    title="Upload Image"
                                                                    uploadUrl="${suffix.path}"
                                                                    useHTML5="{Boolean}true"/>
                                                            </items>
                                                        </column>
                                                    </items>
                                                </field>
                                            </product>
                                        </items>
                                    </column>
                                </items>
                            </fieldset>
                        </items>
                    </column>
                </items>
            </company>
        </items>
    </content>
</jcr:root>


Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/eaem-touchui-image-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/eaem-touchui-image-multifield/clientlib/js.txt, add the following

                         image-multifield.js

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

(function () {
    var DATA_EAEM_NESTED = "data-eaem-nested",
        CFFW = ".coral-Form-fieldwrapper",
        THUMBNAIL_IMG_CLASS = "cq-FileUpload-thumbnail-img",
        SEP_SUFFIX = "-",
        SEL_FILE_UPLOAD = ".coral-FileUpload",
        SEL_FILE_REFERENCE = ".cq-FileUpload-filereference",
        SEL_FILE_NAME = ".cq-FileUpload-filename",
        SEL_FILE_MOVEFROM = ".cq-FileUpload-filemovefrom";

    function getStringBeforeAtSign(str){
        if(_.isEmpty(str)){
            return str;
        }

        if(str.indexOf("@") != -1){
            str = str.substring(0, str.indexOf("@"));
        }

        return str;
    }

    function getStringAfterAtSign(str){
        if(_.isEmpty(str)){
            return str;
        }

        return (str.indexOf("@") != -1) ? str.substring(str.indexOf("@")) : "";
    }

    function getStringAfterLastSlash(str){
        if(!str || (str.indexOf("/") == -1)){
            return "";
        }

        return str.substr(str.lastIndexOf("/") + 1);
    }

    function getStringBeforeLastSlash(str){
        if(!str || (str.indexOf("/") == -1)){
            return "";
        }

        return str.substr(0, str.lastIndexOf("/"));
    }

    function removeFirstDot(str){
        if(str.indexOf(".") != 0){
            return str;
        }

        return str.substr(1);
    }

    function modifyJcrContent(url){
        return url.replace(new RegExp("^" + Granite.HTTP.getContextPath()), "")
                .replace("_jcr_content", "jcr:content");
    }

    function isSelectOne($field) {
        return !_.isEmpty($field) && ($field.prop("type") === "select-one");
    }

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

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

    function isCheckbox($field) {
        return !_.isEmpty($field) && ($field.prop("type") === "checkbox");
    }

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

    function setWidgetValue($field, value) {
        if (_.isEmpty($field)) {
            return;
        }

        if (isSelectOne($field)) {
            setSelectOne($field, value);
        } else if (isCheckbox($field)) {
            setCheckBox($field, value);
        } else {
            $field.val(value);
        }
    }

    /**
     * Removes multifield number suffix and returns just the fileRefName
     * Input: paintingRef-1, Output: paintingRef
     *
     * @param fileRefName
     * @returns {*}
     */
    function getJustName(fileRefName){
        if(!fileRefName || (fileRefName.indexOf(SEP_SUFFIX) == -1)){
            return fileRefName;
        }

        var value = fileRefName.substring(0, fileRefName.lastIndexOf(SEP_SUFFIX));

        if(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1 == fileRefName.length){
            return value;
        }

        return value + fileRefName.substring(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1);
    }

    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){
        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" || _.isObject(fValue)){
                    return;
                }

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

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

                setWidgetValue($field, fValue);
            });
        });
    }

    function buildImageField($multifield, mName){
        $multifield.find(".coral-FileUpload:last").each(function () {
            var $element = $(this), widget = $element.data("fileUpload"),
                resourceURL = $element.parents("form.cq-dialog").attr("action"),
                counter = $multifield.find(SEL_FILE_UPLOAD).length;

            if (!widget) {
                return;
            }

            var fuf = new Granite.FileUploadField(widget, resourceURL);

            addThumbnail(fuf, mName, counter);
        });
    }

    function addThumbnail(imageField, mName, counter){
        var $element = imageField.widget.$element,
            $thumbnail = $element.find("." + THUMBNAIL_IMG_CLASS),
            thumbnailDom;

        $thumbnail.empty();

        $.ajax({
            url: imageField.resourceURL + ".2.json",
            cache: false
        }).done(handler);

        function handler(data){
            var fName = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileName)),
                fRef = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileReference));

            if(isFileNotFilled(data, counter, fRef)){
                return;
            }

            var fileName = data[mName][counter][fName],
                fileRef = data[mName][counter][fRef];

            if (!fileRef) {
                return;
            }

            if (imageField._hasImageMimeType()) {
                imageField._appendThumbnail(fileRef, $thumbnail);
            }

            var $fileName = $element.find("[name=\"" + imageField.fieldNames.fileName + "\"]"),
                $fileRef = $element.find("[name=\"" + imageField.fieldNames.fileReference + "\"]");

            $fileRef.val(fileRef);
            $fileName.val(fileName);
        }

        function isFileNotFilled(data, counter, fRef){
            return _.isEmpty(data[mName])
                    || _.isEmpty(data[mName][counter])
                    || _.isEmpty(data[mName][counter][fRef])
        }
    }

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

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

            workaroundFileInputPositioning($multifields);

            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){
                    $multifield.on("click", ".js-coral-Multifield-add", function () {
                        buildImageField($multifield, mName);
                    });

                    buildMultiField(data[mName], $multifield, mName);
                });
            }
        });
    }

    function workaroundFileInputPositioning($multifields){
        //to workaround the .coral-FileUpload-input positioning issue
        $multifields.find(".js-coral-Multifield-add")
                    .css("position" ,"relative");
    }

    function collectImageFields($form, $fieldSet, counter){
        var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
            return $(ele).find(SEL_FILE_UPLOAD).length == 0;
        });

        $fields.each(function (j, field) {
            var $field = $(field),
                $widget = $field.find(SEL_FILE_UPLOAD).data("fileUpload");

            if(!$widget){
                return;
            }

            var prefix = $fieldSet.data("name") + "/" + (counter + 1) + "/",

                $fileRef = $widget.$element.find(SEL_FILE_REFERENCE),
                refPath = prefix + getJustName($fileRef.attr("name")),

                $fileName = $widget.$element.find(SEL_FILE_NAME),
                namePath = prefix + getJustName($fileName.attr("name")),

                $fileMoveRef = $widget.$element.find(SEL_FILE_MOVEFROM),
                moveSuffix =   $widget.inputElement.attr("name") + "/" + new Date().getTime()
                                        + SEP_SUFFIX + $fileName.val(),
                moveFromPath =  moveSuffix + "@MoveFrom";

            $('<input />').attr('type', 'hidden').attr('name', refPath)
                .attr('value', $fileRef.val() || ($form.attr("action") + removeFirstDot(moveSuffix)))
                .appendTo($form);

            $('<input />').attr('type', 'hidden').attr('name', namePath)
                .attr('value', $fileName.val()).appendTo($form);

            $('<input />').attr('type', 'hidden').attr('name', moveFromPath)
                .attr('value', modifyJcrContent($fileMoveRef.val())).appendTo($form);

            $field.remove();
        });
    }

    function collectNonImageFields($form, $fieldSet, counter){
        var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
            return $(ele).find(SEL_FILE_UPLOAD).length > 0;
        });

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

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

        if (!name) {
            return;
        }

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

        value = $field.val();

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

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

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

    //collect data from widgets in multifield and POST them to CRX
    function collectDataFromFields(){
        $(document).on("click", ".cq-dialog-submit", function () {
            var $multifields = $("[" + DATA_EAEM_NESTED + "]");

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

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

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

                $fieldSets.each(function (counter, fieldSet) {
                    collectNonImageFields($form, $(fieldSet), counter);

                    collectImageFields($form, $(fieldSet), counter);
                });
            });
        });
    }

    function overrideGranite_refreshThumbnail(){
        var prototype = Granite.FileUploadField.prototype,
            ootbFunc = prototype._refreshThumbnail;

        prototype._refreshThumbnail = function() {
            var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]");

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

            return ootbFunc.call(this);
        }
    }

    function overrideGranite_computeFieldNames(){
        var prototype = Granite.FileUploadField.prototype,
            ootbFunc = prototype._computeFieldNames;

        prototype._computeFieldNames = function(){
            ootbFunc.call(this);

            var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]");

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

            var fieldNames = {},
                fileFieldName = $imageMulti.find("input[type=file]").attr("name"),
                counter = $imageMulti.find(SEL_FILE_UPLOAD).length;

            _.each(this.fieldNames, function(value, key){
                if(value.indexOf("./jcr:") == 0){
                    fieldNames[key] = value;
                }else if(key == "tempFileName" || key == "tempFileDelete"){
                    value = value.substring(0, value.indexOf(".sftmp")) + getStringAfterAtSign(value);
                    fieldNames[key] = fileFieldName + removeFirstDot(getStringBeforeAtSign(value))
                                        + SEP_SUFFIX + counter + ".sftmp" + getStringAfterAtSign(value);
                }else{
                    fieldNames[key] = getStringBeforeAtSign(value) + SEP_SUFFIX
                                                    + counter + getStringAfterAtSign(value);
                }
            });

            this.fieldNames = fieldNames;

            this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath);
            this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath) + removeFirstDot(fieldNames.tempFileName);
        }
    }

    function performOverrides(){
        overrideGranite_computeFieldNames();
        overrideGranite_refreshThumbnail();
    }

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

    performOverrides();
})();

9 comments:

  1. When I try to edit the content, the multifield alwais add a new field, do you know why this happening?

    ReplyDelete
  2. thanks for the article it was really helpful !!!

    quick question, whats determining that the nodes get saved as children, instead of a String array as a property

    ReplyDelete
  3. Hi, have the same doubt, need to store the data as a String array property, how to achieve that ?

    ReplyDelete
  4. Thanks this was very helpful, and works on 6.3 as well.

    ReplyDelete
    Replies
    1. Hi Lopes,
      Have you tried it on 6.2?
      Thanks,
      Krass

      Delete
  5. Hi Sreekanth,
    I have been trying to use the Touch UI Composite Image Multifield with AEM 6.2 SP1 CFP4 & CFP5, but facing the problem where once you open the dialog and click "Add Filed" it shows 3 image areas.
    Can you help identify the problem?
    Thanks,
    Krass

    ReplyDelete
    Replies
    1. Looks like the issue is related to the index "-1-1" added after the property "productImageRef" - Example: productImageRef-1-1

      Delete
    2. I have identified the cause of the issue. Apparently, if the clientlib as defined in the step 2, 3, and 4 of the solution, exists more than once in the repository, that will cause the author dialog of each instance of the component, to have multiple image drop areas equal of the total number of occurrences of the above clientlib. The issue has not only visual effect, but also causes functional issues.

      After removing all duplicates from the repository, my Touch UI Composite Image Multifield works fine again.

      I hope that helps to all who have faced the same issue.


      Krass

      Delete
  6. Hi Srikanth,

    Your code is really helpful. How can make it as mandatory field. Tried adding property required:true but its not working for image and for other widgets also( text fields and pathbrowser widgets) can you help me on this

    ReplyDelete