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
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(); } }
Please give me your number so I can call you. I need help asap on this...
ReplyDeletesrikanth please share any example code for nested multifield for classic UI
Deletehttp://experience-aem.blogspot.com/2015/06/aem-61-classic-ui-nested-composite-multifield-panel.html
DeleteThis comment has been removed by the author.
ReplyDeleteThanks for your effort, it really does help on my project.
ReplyDeleteBut Is it possible to add another textfield in the multifield?
KO, check this post http://experience-aem.blogspot.com/2014/04/aem-cq-56-slide-show-component.html
DeleteOh yea SREEK!! your the man bro :-)
DeleteHi Sreekant,
ReplyDeleteDo 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.
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
DeleteHi Sreekant,
ReplyDeleteThanks 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
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
DeleteHello 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
ReplyDeleteHello 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
DeleteHi Sreekanth,
ReplyDeleteI 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.
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
DeleteYes 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.
DeleteThe 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???
DeleteSee my attempt below, this works im not sure if it "good" Java though, i am a frontender :P
This comment has been removed by the author.
ReplyDeleteCan you explain how java code is called and when it triggered(Write layer)
ReplyDeleteHi Sreekanth ,
ReplyDeleteThis dialog breaks when author adds an image from local machine/desktop. Can you please let me know how to fix that?
Thanks,
Amit
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
DeleteHi Sreekanth,
DeleteThanks, that would be great because i am stuck at this issue during my implementation.
Hi Sreekanth,
DeleteFirst 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
Also interested on this
DeleteThis comment has been removed by the author.
ReplyDeletevery interested post
ReplyDeletewebsite designing company in india
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 !
ReplyDeleteThe same problem persist for the simple slideshow package as well. Will be great to have that fixed as well :-) Thanks !
ReplyDeleteTo 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:
ReplyDelete1. 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
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 !
ReplyDeleteHi 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.
ReplyDeleteimage 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
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
ReplyDeleteat 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