AEM 61 - Touch UI 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 62 Image Multifield check this post

Hotfix 6670 must be installed for this widget extension to work

For a slideshow component check this post

Demo | Package Install


Bug Fixes

Support for File Upload in Multifield - Demo | Package Install

Select and Checkbox fixes - Demo | Package Install

Image Multifield for 61 SP1 (no hotfixes necessary) - Demo | Package Install


Image Mulitifield



Nodes in CRX



Dialog



Dialog XML

#49 makes the widget a composite multifield by adding empty valued flag eaem-nested
#78 fileReferenceParameter for storing image path

<?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="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">
            <gallery
                jcr:primaryType="nt:unstructured"
                jcr:title="Gallery"
                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="India Dashboard"
                                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">
                                            <gallery
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                fieldDescription="Enter Gallery Name"
                                                fieldLabel="Gallery"
                                                name="./gallery"/>
                                            <artist
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/multifield"
                                                class="full-width"
                                                eaem-nested=""
                                                fieldDescription="Click '+' to add a Artist"
                                                fieldLabel="URLs">
                                                <field
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                                    name="./artists">
                                                    <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">
                                                                <artist
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                                    fieldDescription="Enter Artist Name"
                                                                    fieldLabel="Artist"
                                                                    name="./artist"/>
                                                                <painting
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/fileupload"
                                                                    autoStart="{Boolean}false"
                                                                    class="cq-droptarget"
                                                                    fieldLabel="Painting"
                                                                    fileNameParameter="./paintingName"
                                                                    fileReferenceParameter="./paintingRef"
                                                                    mimeTypes="[image]"
                                                                    multiple="{Boolean}false"
                                                                    name="./painting"
                                                                    title="Upload Image"
                                                                    uploadUrl="${suffix.path}"
                                                                    useHTML5="{Boolean}true"/>
                                                            </items>
                                                        </column>
                                                    </items>
                                                </field>
                                            </artist>
                                        </items>
                                    </column>
                                </items>
                            </fieldset>
                        </items>
                    </column>
                </items>
            </gallery>
        </items>
    </content>
</jcr:root>


Component Rendering



Solution


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

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

                         image-multifield.js

4) Create file ( type nt:file ) /apps/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");
    }

    /**
     * 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;
                }

                $field.val(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) {
                if (imageField._isImageMimeType(fileRef)) {
                    thumbnailDom = imageField._createImageThumbnailDom(fileRef);
                } else {
                    thumbnailDom = $("<p>" + fileRef + "</p>");
                }
            }

            if (!thumbnailDom) {
                return;
            }

            $element.addClass("is-filled");
            $thumbnail.append(thumbnailDom);

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

            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 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");

        if (!name) {
            return;
        }

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

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

        $('<input />').attr('type', 'hidden')
            .attr('name', fieldSetName + "/" + counter + "/" + name)
            .attr('value', $field.val())
            .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_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"),
                $fieldSet = $imageMulti.find(SEL_FILE_UPLOAD).closest("[class='coral-Form-fieldset']"),
                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);
        }
    }

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

    overrideGranite_computeFieldNames();
})();


40 comments:

  1. How to sync the data between richtext inside multi field in classic UI and with touch UI?

    ReplyDelete
  2. Unable to find Hotfix 6670. Tried on https://helpx.adobe.com/experience-manager/kb/aem61-available-hotfixes.html and package share. Could you please share a direct link?

    ReplyDelete
    Replies
    1. http://localhost:4502/crx/packageshare/index.html/content/marketplace/marketplaceProxy.html?packagePath=/content/companies/private/day_internal/packages/adobe/cq610/hotfix/cq-6.1.0-hotfix-6670

      Delete
    2. Hi,

      I still can't access the Hotfix on the direct link provided above.Kindly Assist!

      Delete
    3. Hello, create (and login) with adobe id; if you see any error message, add the above url in address bar and refresh (after login)

      Delete
    4. Hi ,
      Plz provide the hotfix 6670 fix....this link seems to be broken

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

      Delete
    6. shivali, mah8473 - you may have to contact daycare for getting the hotfix package; not official adobe blog, to discuss issues addressed by the hotfix, daycare should be able to provide with necessary answers

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

    ReplyDelete
  4. I still can't access the Hotfix. I tried the direct link, still nothing.
    Kindly assist.

    ReplyDelete
  5. I have applied hotfixes in AEM 6.1, and tried this functionality. It works fine as expected. After a day, this functionality is broken as before hotfix applied.

    ReplyDelete
    Replies
    1. Hi,

      Can you provide me the hotfix 6670 package ?

      Delete
    2. Hi, I am able to drag and drop the multiple 'Upload Asset' images in a single dialog, but the images are not being saved correctly and not being displayed correctly after dialog submit..

      Please provide the solution for the same.

      Delete
  6. Please assist me if you have any work around the functionality

    ReplyDelete
    Replies
    1. http://localhost:4502/crx/packageshare/index.html/content/marketplace/marketplaceProxy.html?packagePath=/content/companies/public/adobe/packages/cq610/featurepack/cq-6.1.0-featurepack-6563

      Try this : NPR-6670 - Hotfix for 6.1 #CQ-41631: Can't place multiple fileupload widgets on the same dialog

      Delete
  7. How to disable file upload from file system in file upload button?

    ReplyDelete
    Replies
    1. In 61, remove the uploadUrl property on widget node (sling:resourceType=granite/ui/components/foundation/form/fileupload) to disable the “Upload Image Asset” button

      Delete
    2. Use disabled={Boolean}true property.
      This will disable upload from file system

      Delete
  8. Thanks Sreekanth. Its works fine. Can we remove file upload button completely? Once we remove it, will have placeholder image on the dialog. FYI - I have tried it with placeholder property to have placeholder image. it's not working. let me know if you have any thoughts on my approach

    ReplyDelete
  9. Hi Sreekanth, I would like to fix the below issue in multifiled image component. Please provide me your thoughts.

    1. Create new page, and add the multifield component to the page
    2. Click on dialog configure icon, and click on Add field button
    3. Drag and drop the image to dialog. -> We are not able to drop the image on the dialog.

    We can drop the images with the below approach.
    1. Create new page, and add the multifield component to the page
    2. Click on dialog configure icon, and click on Add field button
    3. Click on save icon(tick icon) on the dialog
    4. Again click on dialog configure icon - dialog will load the empty field set
    5. Drag and drop the images - It accepting the image.

    Let me know if you have any fix on the issue

    ReplyDelete
  10. HI Sreekanth, I am planning to covert the existing custom widget into the touch ui, could you please guide me how can I make it as touch ui compatible. Please let me know if I can share my code on your email address.

    ReplyDelete
  11. HI Sreekanth, I am planning to covert the existing custom widget into the touch ui, could you please guide me how can I make it as touch ui compatible. Please let me know if I can share my code on your email address.

    ReplyDelete
  12. Sreekanth the hotfix packagePath=/content/companies/private/day_internal/packages/adobe/cq610/hotfix/cq-6.1.0-hotfix-6670 appears to be private/internal? How can I get hold of this? And what issues does it address

    ReplyDelete
  13. Hi Sreekanth, I have been using your sample code in AEM 6.1 with the hotfixes you suggest. When I create an image field and drag the image over I see 3 drop zones created. When I drop the image I see 3 thumbnails. After saving I see 1 image in the JCR but when I re-edit the component I see 3 image fields but only the first one has a thumbnail. After inspecting the markup I see that the process of creating the image field after hitting the Add button creates 3 sets of markup for the drop target. Any idea why this might happen? I am an Adobe employee working with AEM. Thanks, Chris

    ReplyDelete
    Replies
    1. hi Chris, can you email the package with dialog you are testing to my adobe email

      Delete
    2. Hi Sreekanth,
      I am having the same issue as Chris. Would be great if you could share how you've resolved this.
      Thanks,
      Alex

      Delete
    3. Hi Sreekanth,
      I am facing the same issue, can you tell me how this can be resolved ?
      Thanks

      Delete
  14. Hi Sreekanth, I am unable to add checkbox in a multifield,
    The checkbox doesnot store any value..

    ReplyDelete
  15. Sreekanth, Curious if you have considered writing the component using sightly markup rather than JSP? Any chance you could share a working version if you have?

    ReplyDelete
    Replies
    1. hi, that would be cool, sorry haven't coded one...

      Delete
    2. http://experience-aem.blogspot.com/2015/12/aem-61-touchui-slide-show-component-using-sightly-image-multifield.html

      Delete
  16. hi sreekanth,
    your blog is really useful. We have a requirement to create custom multifields with 2 images inside a multifield. Like backgroundImage and foregroundImage within same multifield. I see your js for touchui is only populating for last image. This is due to the code:

    function buildImageField($multifield, mName){
    $multifield.find(".coral-FileUpload:last").each(function () {

    js file looks for last file upload and updates. Previous images within same multifield are ignored. Can you please help to fix this so as to work for any number of images within custom multifield?

    ReplyDelete
  17. Hi sreekanth,
    I am able to drag and drop the multiple 'Upload Asset' images in a single dialog, but the images are not being saved correctly and not being displayed correctly after dialog submit..
    I have not applied the hotfix 6670...as I have not found it anywhere.
    Please suggest any possible solution for the same.

    ReplyDelete
    Replies
    1. I am getting the below error on the component after dialog submit:

      Image Multi Field Sample

      artist : h1 undefined : javax.jcr.ValueFormatException: undefined = [/content/geometrixx-outdoors/en/_jcr_content/par/sample_image_multifi/painting/1459256607483-undefined, ] is multi-valued. at org.apache.jackrabbit.oak.jcr.delegate.PropertyDelegate.getSingleState(PropertyDelegate.java:112) at org.apache.jackrabbit.oak.jcr.session.PropertyImpl$5.perform(PropertyImpl.java:251) at org.apache.jackrabbit.oak.jcr.session.PropertyImpl$5.perform(PropertyImpl.java:247) at org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate.perform(SessionDelegate.java:202) at org.apache.jackrabbit.oak.jcr.session.ItemImpl.perform(ItemImpl.java:112) at org.apache.jackrabbit.oak.jcr.session.PropertyImpl.getValue(PropertyImpl.java:247) at org.apache.jackrabbit.oak.jcr.session.PropertyImpl.getString(PropertyImpl.java:273) at org.apache.jsp.apps.touchui_002dimage_002dmultifield.sample_002dimage_002dmultifield.sample_002dimage_002dmultifield_jsp._jspService(sample_002dimage_002dmultifield_jsp.java:193) at org.apache.sling.scripting.jsp.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70) at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)

      Delete
  18. If you install HF 9084 on top of a 6.1 SP1 instance, this will break because the method _createImageThumbnailDom has been removed from the Granite.FileUploadFiled widget. You will have to replace the line thumbnailDom = imageField._createImageThumbnailDom(fileRef); with thumbnailDom = imageField._createThumbnailFromRendition(fileRef, $thumbnail);

    ReplyDelete
  19. Hi Sreekanth,

    The widget works fine and saves the data on dialog submit. However, when I open the dialog again, the images aren't visible.
    Any solution around this?

    Thanks
    Ruchi

    ReplyDelete