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
For AEM 6440 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.dialog, dependencies 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(); })();
How to sync the data between richtext inside multi field in classic UI and with touch UI?
ReplyDeleteUnable 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?
ReplyDeletehttp://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
DeleteHi,
DeleteI still can't access the Hotfix on the direct link provided above.Kindly Assist!
Hello, create (and login) with adobe id; if you see any error message, add the above url in address bar and refresh (after login)
DeleteHi ,
DeletePlz provide the hotfix 6670 fix....this link seems to be broken
This comment has been removed by the author.
Deleteshivali, 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
DeleteThis comment has been removed by the author.
ReplyDeleteI still can't access the Hotfix. I tried the direct link, still nothing.
ReplyDeleteKindly assist.
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.
ReplyDeleteHi,
DeleteCan you provide me the hotfix 6670 package ?
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..
DeletePlease provide the solution for the same.
Please assist me if you have any work around the functionality
ReplyDeleteDo you have hotfix 6670?
Deletehttp://localhost:4502/crx/packageshare/index.html/content/marketplace/marketplaceProxy.html?packagePath=/content/companies/public/adobe/packages/cq610/featurepack/cq-6.1.0-featurepack-6563
DeleteTry this : NPR-6670 - Hotfix for 6.1 #CQ-41631: Can't place multiple fileupload widgets on the same dialog
How to disable file upload from file system in file upload button?
ReplyDeleteIn 61, remove the uploadUrl property on widget node (sling:resourceType=granite/ui/components/foundation/form/fileupload) to disable the “Upload Image Asset” button
DeleteUse disabled={Boolean}true property.
DeleteThis will disable upload from file system
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
ReplyDeleteHi Sreekanth, I would like to fix the below issue in multifiled image component. Please provide me your thoughts.
ReplyDelete1. 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
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.
ReplyDeleteHI 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.
ReplyDeleteSreekanth 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
ReplyDeleteHi 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
ReplyDeletehi Chris, can you email the package with dialog you are testing to my adobe email
DeleteHi Sreekanth,
DeleteI am having the same issue as Chris. Would be great if you could share how you've resolved this.
Thanks,
Alex
Hi Sreekanth,
DeleteI am facing the same issue, can you tell me how this can be resolved ?
Thanks
Hi Sreekanth,
DeleteI am facing the same issue, can you tell me how this can be resolved ?in aem 6.4.0
Thanks
https://experience-aem.blogspot.com/2019/04/aem-6440-touch-ui-composite-image-multifield.html
DeleteHi Sreekanth, I am unable to add checkbox in a multifield,
ReplyDeleteThe checkbox doesnot store any value..
Hi, check the bugfixes section
DeleteSreekanth, 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?
ReplyDeletehi, that would be cool, sorry haven't coded one...
Deletehttp://experience-aem.blogspot.com/2015/12/aem-61-touchui-slide-show-component-using-sightly-image-multifield.html
Deletehi sreekanth,
ReplyDeleteyour 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?
Hi sreekanth,
ReplyDeleteI 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.
I am getting the below error on the component after dialog submit:
DeleteImage 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)
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);
ReplyDeleteHi Sreekanth,
ReplyDeleteThe 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