AEM CQ 56 - Adding Images in MultiField

Goal


This post is on adding multiple images in a component dialog. Here we extend and use CQ.form.MultiField to add images of type CQ.html5.form.SmartImage. Source Code, Package Install and Demo Video are available for download

A nice slide show component created with image multi-field is available here

For Touch UI check this post

Add images using drag and drop from content finder, as the upload functionality in image widget has a bug yet to be fixed




Prerequisities


If you are new to CQ visit this blog post; it explains page component basics and setting up your IDE

Create Component


1) In your CRXDE Lite http://localhost:4502/crx/de, create below folder and save changes

                      /apps/imagemultifield

2) Copy the component /libs/foundation/components/logo and paste it in path /apps/imagemultifield

3) Rename /apps/imagemultifield/logo to /apps/imagemultifield/imagemultifield

4) Rename /apps/imagemultifield/imagemultifield/logo.jsp to /apps/imagemultifield/imagemultifield/imagemultifield.jsp

5) Change the following properties of /apps/imagemultifield/imagemultifield

                     componentGroup - My Components
                     jcr:title - Image MultiField Component

6) Add the following to dialog (/apps/imagemultifield/imagemultifield/dialog) xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Dialog"
    activeTab="{Long}0"
    title="Multi Image"
    xtype="tabpanel">
    <items jcr:primaryType="cq:WidgetCollection">
        <basic
            jcr:primaryType="cq:Widget"
            title="Images"
            xtype="panel">
            <items jcr:primaryType="cq:WidgetCollection">
                <images
                    jcr:primaryType="cq:Widget"
                    border="false"
                    hideLabel="true"
                    name="./images"
                    xtype="imagemultifield">
                    <fieldConfig
                        jcr:primaryType="cq:Widget"
                        border="false"
                        hideLabel="true"
                        layout="form"
                        padding="10px 0 0 100px"
                        xtype="imagemultifieldpanel">
                        <items jcr:primaryType="cq:WidgetCollection">
                            <image
                                jcr:primaryType="cq:Widget"
                                cropParameter="./imageCrop"
                                ddGroups="[media]"
                                fileNameParameter="./imageName"
                                fileReferenceParameter="./imageReference"
                                height="250"
                                mapParameter="./imageMap"
                                name="./image"
                                rotateParameter="./imageRotate"
                                sizeLimit="100"
                                xtype="imagemultifieldsmartimage"/>
                        </items>
                    </fieldConfig>
                </images>
            </items>
        </basic>
    </items>
</jcr:root>

8) Line 18, create an instance of widget type imagemultifield ( ImageMultiField.MultiField extending CQ.form.MultiField) explained in the next section

9) Line 25, 38, when a user clicks on Add Item of imagemultifield, create a panel of type imagemultifieldpanel (ImageMultiField.Panel extending CQ.Ext.Panel); in the panel created, add image widget imagemultifieldsmartimage ( ImageMultiField.SmartImage extending CQ.html5.form.SmartImage)

Add JS Logic and Register XTypes


1) Create node /apps/imagemultifield/imagemultifield/clientlib of type cq:ClientLibraryFolder and add the following properties

             categories - String - cq.widgets

2) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/js.txt and add the following

              imagemultifield.js

3) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/imagemultifield.js and add the following code

CQ.Ext.ns("ImageMultiField");

ImageMultiField.Panel = CQ.Ext.extend(CQ.Ext.Panel, {
    initComponent: function () {
        ImageMultiField.Panel.superclass.initComponent.call(this);

        var multifield = this.findParentByType('imagemultifield');
        var image = this.find('xtype', 'imagemultifieldsmartimage')[0];

        var imageName = multifield.nextImageName;

        if(!imageName){
            imageName = image.name;

            if(!imageName){
                imageName = "demo";
            }else if(imageName.indexOf("./") == 0){
                imageName = imageName.substr(2); //get rid of ./
            }

            var suffix = multifield.nextImageNum = multifield.nextImageNum + 1;
            imageName = this.name + "/" + imageName + "-" + suffix;
        }

        image.name = imageName;

        var changeParams = ["cropParameter", "fileNameParameter","fileReferenceParameter",
                                "mapParameter","rotateParameter" ];

        CQ.Ext.each(changeParams, function(cItem){
            if(image[cItem]){
                image[cItem] = imageName + "/" +
                    ( image[cItem].indexOf("./") == 0 ? image[cItem].substr(2) : image[cItem]);
            }
        });

        CQ.Ext.each(image.imageToolDefs, function(toolDef){
            toolDef.transferFieldName = imageName + toolDef.transferFieldName.substr(1);
            toolDef.transferField.name = toolDef.transferFieldName;
        });
    },

    setValue: function (record) {
        var multifield = this.findParentByType('imagemultifield');
        var image = this.find('xtype', 'imagemultifieldsmartimage')[0];

        var recCopy = CQ.Util.copyObject(record);

        var imagePath = multifield.path + "/" + image.name;
        var imgRec = recCopy.get(image.name);

        for(var x in imgRec){
            if(imgRec.hasOwnProperty(x)){
                recCopy.data[x] = imgRec[x];
            }
        }

        recCopy.data[this.name.substr(2)] = undefined;

        var fileRefParam = image.fileReferenceParameter;
        image.fileReferenceParameter = fileRefParam.substr(fileRefParam.lastIndexOf("/") + 1);

        image.processRecord(recCopy, imagePath);
        image.fileReferenceParameter = fileRefParam;
    },

    validate: function(){
        return true;
    }
});

CQ.Ext.reg("imagemultifieldpanel", ImageMultiField.Panel);

ImageMultiField.SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
    syncFormElements: function() {
        if(!this.fileNameField.getEl().dom){
            return;
        }

        ImageMultiField.SmartImage.superclass.syncFormElements.call(this);
    } ,

    afterRender: function() {
        ImageMultiField.SmartImage.superclass.afterRender.call(this);

        var dialog = this.findParentByType('dialog');
        var target = this.dropTargets[0];

        if (dialog && dialog.el && target.highlight) {
            var dialogZIndex = parseInt(dialog.el.getStyle("z-index"), 10);

            if (!isNaN(dialogZIndex)) {
                target.highlight.zIndex = dialogZIndex + 1;
            }
        }

        var multifield = this.findParentByType('multifield');
        multifield.dropTargets.push(target);

        this.dropTargets = undefined;
    }
});

CQ.Ext.reg('imagemultifieldsmartimage', ImageMultiField.SmartImage);

CQ.Ext.override(CQ.form.SmartImage.ImagePanel, {
    addCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);

        if(imageCanvas){
            imageCanvas.addClass(clazz);
        }
    },

    removeCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);

        if(imageCanvas){
            imageCanvas.removeClass(clazz);
        }
    }
});

CQ.Ext.override(CQ.form.SmartImage.Tool, {
    processRecord: function(record) {
        var iniValue = record.get(this.transferFieldName);

        if(!iniValue && ( this.transferFieldName.indexOf("/") !== -1 )){
            iniValue = record.get(this.transferFieldName.substr(this.transferFieldName.lastIndexOf("/") + 1));
        }

        if (iniValue == null) {
            iniValue = "";
        }

        this.initialValue = iniValue;
    }
});

CQ.Ext.override(CQ.form.MultiField.Item, {
    reorder: function(item) {
        if(item.field && item.field.xtype == "imagemultifieldpanel"){
            var c = this.ownerCt;
            var iIndex = c.items.indexOf(item);
            var tIndex = c.items.indexOf(this);

            if(iIndex < tIndex){ //user clicked up
                c.insert(c.items.indexOf(item), this);
                this.getEl().insertBefore(item.getEl());
            }else{//user clicked down
                c.insert(c.items.indexOf(this), item);
                this.getEl().insertAfter(item.getEl());
            }

            c.doLayout();
        }else{
            var value = item.field.getValue();
            item.field.setValue(this.field.getValue());
            this.field.setValue(value);
        }
    }
});

ImageMultiField.MultiField = CQ.Ext.extend(CQ.form.MultiField , {
    Record: CQ.data.SlingRecord.create([]),
    nextImageNum: 0,
    nextImageName: undefined,

    initComponent: function() {
        ImageMultiField.MultiField.superclass.initComponent.call(this);

        var imagesOrder = new CQ.Ext.form.Hidden({
            name: this.getName() + "/order"
        });

        this.add(imagesOrder);

        var dialog = this.findParentByType('dialog');

        dialog.on('beforesubmit', function(){
            var imagesInOrder = this.find('xtype','imagemultifieldsmartimage');
            var order = [];

            CQ.Ext.each(imagesInOrder , function(image){
                order.push(image.name.substr(image.name.lastIndexOf("/") + 1))
            });

            imagesOrder.setValue(JSON.stringify(order));
        },this);

        this.dropTargets = [];
    },

    addItem: function(value){
        if(!value){
            value = new this.Record({},{});
        }
        ImageMultiField.MultiField.superclass.addItem.call(this, value);
    },

    processRecord: function(record, path) {
        if (this.fireEvent('beforeloadcontent', this, record, path) !== false) {
            this.items.each(function(item) {
                if(item.field && item.field.xtype == "imagemultifieldpanel"){
                    this.remove(item, true);
                }
            }, this);

            var images = record.get(this.getName());
            this.nextImageNum = 0;

            if (images) {
                var oName = this.getName() + "/order";
                var oValue = record.get(oName) ? record.get(oName) : "";

                var iNames = JSON.parse(oValue);
                var highNum, val;

                CQ.Ext.each(iNames, function(iName){
                    val = parseInt(iName.substr(iName.indexOf("-") + 1));

                    if(!highNum || highNum < val){
                        highNum = val;
                    }

                    this.nextImageName = this.getName() + "/" + iName;
                    this.addItem(record);
                }, this);

                this.nextImageNum = highNum;
            }

            this.nextImageName = undefined;

            this.fireEvent('loadcontent', this, record, path);
        }
    }
});

CQ.Ext.reg('imagemultifield', ImageMultiField.MultiField);

4) In the above step we are adding necessary js logic to create a multifield of images; each item of multifield is a panel holding one smart image.So, when user clicks on Add Item a panel is created and added to multifield, smart image added to the panel

Content Structure in CRX


1) When you add images using the above imagemultifield with default configuration, content structure gets created in CRX  - /content/multi-image/jcr:content/par/imagemultifield

2) The below node at level /content/multi-image/jcr:content/par/imagemultifield/images stores image order for rendering images in the order they were added or reordered



3)  Each image is added as a separate node under imagemultifield/images. For example /content/multi-image/jcr:content/par/imagemultifield/images/image-2

Rendering Images


1) Images created in the CRX using MultiImage componet are rendered using /apps/imagemultifield/imagemultifield/imagemultifield.jsp. Add the following code in jsp

<%@include file="/libs/foundation/global.jsp"%>

<%@ page import="java.util.Iterator" %>
<%@ page import="com.day.cq.wcm.foundation.Image" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>

<%
    Iterator<Resource> children = resource.listChildren();

    if(!children.hasNext()){
%>

        Configure Images

<%
    }else{
        Resource imagesResource = children.next();
        ValueMap map = imagesResource.adaptTo(ValueMap.class);
        String order = map.get("order", String.class);

        Image img = null; String src = null;
        JSONArray array = new JSONArray(order);

        for(int i = 0; i < array.length(); i++){
            img = new Image(resource);
            img.setItemName(Image.PN_REFERENCE, "imageReference");
            img.setSuffix(String.valueOf(array.get(i)));
            img.setSelector("img");

            src = img.getSrc();
%>
            <img src='<%=src%>'/>
<%
        }
    }
%>


2) Jsp renders, for example, the following image source paths

<img src='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-1'/>
<img src='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-2'/>
<img src='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-3'/>

3) Add the following code in /apps/imagemultifield/imagemultifield/img.GET.java to get image binary

package apps.imagemultifield.imagemultifield;

import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.servlet.http.HttpServletResponse;

import com.day.cq.wcm.foundation.Image;
import com.day.cq.wcm.commons.AbstractImageServlet;
import com.day.image.Layer;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;

public class img_GET extends AbstractImageServlet {

    protected Layer createLayer(ImageContext c)
            throws RepositoryException, IOException {
        return null;
    }

    protected void writeLayer(SlingHttpServletRequest req, SlingHttpServletResponse resp, ImageContext c, Layer layer)
            throws IOException, RepositoryException {
        Iterator<Resource> children = c.resource.listChildren();

        if(!children.hasNext()){
            return;
        }

        String rUri = req.getRequestURI();
        String selImage = rUri.substring(rUri.lastIndexOf("/"));
        Resource resource = req.getResourceResolver().getResource(children.next().getPath() + selImage);

        if(resource == null){
            return;
        }

        Image image = new Image(resource);
        image.setItemName(Image.NN_FILE, "image");
        image.setItemName(Image.PN_REFERENCE, "imageReference");

        if (!image.hasContent()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        image.set(Image.PN_MIN_WIDTH, c.properties.get("minWidth", ""));
        image.set(Image.PN_MIN_HEIGHT, c.properties.get("minHeight", ""));
        image.set(Image.PN_MAX_WIDTH, c.properties.get("maxWidth", ""));
        image.set(Image.PN_MAX_HEIGHT, c.properties.get("maxHeight", ""));

        layer = image.getLayer(false, false, false);

        boolean modified = image.crop(layer) != null;

        modified |= image.resize(layer) != null;

        modified |= image.rotate(layer) != null;

        if (modified) {
            resp.setContentType(c.requestImageType);
            layer.write(c.requestImageType, 1.0, resp.getOutputStream());
        } else {
            Property data = image.getData();
            InputStream in = data.getStream();
            resp.setContentLength((int) data.getLength());
            String contentType = image.getMimeType();

            if (contentType.equals("application/octet-stream")) {
                contentType=c.requestImageType;
            }

            resp.setContentType(contentType);
            IOUtils.copy(in, resp.getOutputStream());

            in.close();
        }

        resp.flushBuffer();
    }
}





32 comments:

  1. Please give me your number so I can call you. I need help asap on this...

    ReplyDelete
    Replies
    1. srikanth please share any example code for nested multifield for classic UI

      Delete
    2. http://experience-aem.blogspot.com/2015/06/aem-61-classic-ui-nested-composite-multifield-panel.html

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

    ReplyDelete
  3. Thanks for your effort, it really does help on my project.

    But Is it possible to add another textfield in the multifield?

    ReplyDelete
    Replies
    1. KO, check this post http://experience-aem.blogspot.com/2014/04/aem-cq-56-slide-show-component.html

      Delete
    2. Oh yea SREEK!! your the man bro :-)

      Delete
  4. Hi Sreekant,

    Do you know how I can create multiple image in the single Multifield. In you example you have one image field per multifield, I need mutiple images in singe multifield panel.

    ReplyDelete
    Replies
    1. hello MG, i never tried using this widget in a multifield (so basically imagemultifield in multifield)... it's worth a try, not sure if any changes might be required to make this imagemultifield work in otb multifield

      Delete
  5. Hi Sreekant,

    Thanks for replying, I think i didn't explained it correct, I trying to have mutiple image with each multipleimage selector. Currently in each mutifield you have one image and then if you need to add another image you click on "+" button. What I would like to have 3 images per multified, since I am creating a carousel to support responsive design and user can provide 3 image per carousel to achieve responsiveness for different break points and carousel can have any number of image for slideshow.
    I tried using you component and problem is it works fine for out-of-box image (first image), but for the rest image, which I added to the component selector, image selector doesn't shows up.
    Do you have any solution for this.

    Thanks for you help in advance.

    -MG

    ReplyDelete
    Replies
    1. hello MG, check this post http://experience-aem.blogspot.com/2014/04/aem-cq-56-slide-show-component.html, have added support for multiple images in each multifield item

      Delete
  6. Hello Sreekanth I have to develop a dynamic gallery component which takes multiple Images and title inputs from author and then displayed them as thumbnails on the page. Could you please guide me how to achieve this.Also please share your mobile/email with me. I would like to discuss with you on this

    ReplyDelete
    Replies
    1. Hello Abhi, the slide show component discussed here http://experience-aem.blogspot.com/2014/04/aem-cq-56-slide-show-component.html can be configured to accept multiple images and text inputs, check the section "Multiple Images in Multifield item" of the post

      Delete
  7. Hi Sreekanth,

    I have tried developing the same component following your instructions, but the component is not rendering the images on the page. I do see the images saved in the jcr repository, but the same are not displayed on the screen. I am using AEM 5.6. Am I missing something ? Please send me your contact number to suresh.ravuru@gmail.com. Thanks.

    ReplyDelete
    Replies
    1. hello Suresh, can you please paste the rendered image url here... also you may want to check the advanced multi image component discussed here http://experience-aem.blogspot.com/2014/04/aem-cq-56-slide-show-component.html

      Delete
    2. Yes Sreekanth that slide show component looks fine, but could you please update something about generating thumbnail and display all the uploaded images as gallery on the page.

      Delete
    3. The code above does not generate the paths stored at imageReference for each image node added by the imagemultifield js. I assume something has changed in the JS???
      See my attempt below, this works im not sure if it "good" Java though, i am a frontender :P

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

    ReplyDelete
  9. Can you explain how java code is called and when it triggered(Write layer)

    ReplyDelete
  10. Hi Sreekanth ,

    This dialog breaks when author adds an image from local machine/desktop. Can you please let me know how to fix that?

    Thanks,
    Amit

    ReplyDelete
    Replies
    1. Hello Amit, you have to add a fix in onFileSelected function override of CQ.html5.form.SmartImage.. i'll try to find some time and fix it this week

      Delete
    2. Hi Sreekanth,

      Thanks, that would be great because i am stuck at this issue during my implementation.

      Delete
    3. Hi Sreekanth,
      First of all, great job! Very useful :)
      would be awesome if the file upload functionality could work, I need it for my application ... Any update on this?
      thank you

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

    ReplyDelete
  12. Hi Sreekanth, If you remember you fixed the problem with multi-widget richtext dialog image drag-n-drop for 2 consecutive times without page refresh. I am facing the same issue here as well. A page refresh is required after every image drag and drop on dialog. And if I cancel the dialog and reopen it to drag drop image on it, it becomes stale. Can you please help me here as well with the fix ? Thanks a ton for all your help !

    ReplyDelete
  13. The same problem persist for the simple slideshow package as well. Will be great to have that fixed as well :-) Thanks !

    ReplyDelete
  14. To mention, when I install the slideshow dialog, the empty fields are getting overwritten by the moved item. I think you also applied this fix on another multi-widget component. Will be great to have the 2 fixes for image and slideshow components:

    1. Image drag-drop on dialog does not work without page refresh, except for the first time page load.
    2. On Rearranging, empty dialog fields are getting overwritten by the moved set values.

    I understand that these articles are in true sense very valuable for us, but fixing these will be of great help !
    Thanks in advance for all your help !

    Regards,
    Somnath

    ReplyDelete
  15. Hi Sreekanth, hope you are doing good. It has become a high priority for me to deliver a component similar to this design. At the minimum, can you please find bit of time to fix at least the image drag-drop issue on dialog for the custom multi-widget needing browser refresh every time ? Thanks !

    ReplyDelete
  16. Hi Srikanth, I am running AEM6.1 SP1 and HF's ( cq-6.1.0-hotfix-10832-1.0, cq-6.1.0-hotfix-10890-1.0 ). So I deployed Imagemultifield code package into my local aem instance.
    image order is ["2","3","1","5","4"] persisted in node property in this way. And adding 6th multifield Item in dailog and author the content and submit ok button, I am getting below error popup on page.

    ERROR WHILE PROCESSING /CONTENT/GEOMETRIXX/EN/JCR:CONTENT/PAR/SIMPLE_SLIDESHOW_1891985970
    Status
    500
    Message
    org.apache.sling.api.SlingException: Exception during response processing.
    Location /content/geometrixx/en/_jcr_content/par/simple_slideshow_1891985970
    Parent Location /content/geometrixx/en/_jcr_content/par
    Path
    /content/geometrixx/en/jcr:content/par/simple_slideshow_1891985970
    Referer http://localhost:4502/content/geometrixx/en.html?cq_ck=1496157971941
    ChangeLog
    Go Back
    Modified Resource
    Parent of Modified Resource

    And exception thrown in error log file like this:

    at java.lang.Thread.run(Unknown Source)
    30.05.2017 20:21:00.570 *ERROR* [0:0:0:0:0:0:0:1 [1496155860559] POST /content/geometrixx/en/jcr:content/par/simple_slideshow HTTP/1.1] org.apache.sling.servlets.post.impl.operations.ModifyOperation Exception during response processing.
    javax.jcr.ValueFormatException: sling:resourceType = [foundation/components/parbase, foundation/components/parbase] 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)

    I don't understand why this error is coming. I requested you to provide solution for and fix for the problem

    Thanks in advance for all your help!

    Thanks,
    Prasad

    ReplyDelete
  17. Hi Srikanth, I am getting an issue when one item is removed and then, I try to add a new one. it seems related to getEL Uncaught TypeError: Cannot read property 'getBoundingClientRect' of null
    at Object.getXY (widgets.min.js:formatted:1363)
    at Function.CQ.Ext.lib.Region.getRegion (widgets.min.js:formatted:7287)
    at constructor.CF_storeDropTargets (widgets.min.js:formatted:86069)
    at widgets.min.js:formatted:86113
    getXY

    ReplyDelete