Goal
Create a Touch UI Composite Multifield configuration supporting Images, widgets of type granite/ui/components/foundation/form/fileupload
For Classic UI Image Multifield check this post
For AEM 61 Touch UI Image Multifield check this post
For AEM 64 check this post
Demo | Package Install
Component Rendering
Image Multifield Structure
Nodes in CRX
Dialog
Dialog XML
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" jcr:title="62 Touch UI Image Multi Field" sling:resourceType="cq/gui/components/authoring/dialog" helpPath="en/cq/current/wcm/default_components.html#Text"> <content jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/tabs" type="nav"/> <items jcr:primaryType="nt:unstructured"> <company jcr:primaryType="nt:unstructured" jcr:title="Company" sling:resourceType="granite/ui/components/foundation/section"> <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <items jcr:primaryType="nt:unstructured"> <fieldset jcr:primaryType="nt:unstructured" jcr:title="Products" sling:resourceType="granite/ui/components/foundation/form/fieldset"> <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <items jcr:primaryType="nt:unstructured"> <company jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/textfield" fieldDescription="Enter Company Name" fieldLabel="Company" name="./company"/> <product jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/multifield" class="full-width" eaem-nested="" fieldDescription="Click '+' to add a Product" fieldLabel="Product"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/fieldset" name="./products"> <layout jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns" method="absolute"/> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <items jcr:primaryType="nt:unstructured"> <productName jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/textfield" fieldDescription="Enter Product Name" fieldLabel="Product" name="./productName"/> <language jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/select" class="language" fieldLabel="Language" name="./language"> <datasource jcr:primaryType="nt:unstructured" sling:resourceType="cq/gui/components/common/datasources/languages" addNone="{Boolean}true"/> </language> <show jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/checkbox" name="./show" text="Show" value="yes"/> <productImage jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/fileupload" autoStart="{Boolean}false" class="cq-droptarget" fieldLabel="Product" fileNameParameter="./productImageName" fileReferenceParameter="./productImageRef" mimeTypes="[image]" multiple="{Boolean}false" name="./productImage" title="Upload Image" uploadUrl="${suffix.path}" useHTML5="{Boolean}true"/> </items> </column> </items> </field> </product> </items> </column> </items> </fieldset> </items> </column> </items> </company> </items> </content> </jcr:root>
Solution
1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-image-multifield
2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-image-multifield/clientlib and set a property categories of String type to cq.authoring.dialog, dependencies of type String[] with value underscore
3) Create file ( type nt:file ) /apps/eaem-touchui-image-multifield/clientlib/js.txt, add the following
image-multifield.js
4) Create file ( type nt:file ) /apps/eaem-touchui-image-multifield/clientlib/image-multifield.js, add the following code
(function () { var DATA_EAEM_NESTED = "data-eaem-nested", CFFW = ".coral-Form-fieldwrapper", THUMBNAIL_IMG_CLASS = "cq-FileUpload-thumbnail-img", SEP_SUFFIX = "-", SEL_FILE_UPLOAD = ".coral-FileUpload", SEL_FILE_REFERENCE = ".cq-FileUpload-filereference", SEL_FILE_NAME = ".cq-FileUpload-filename", SEL_FILE_MOVEFROM = ".cq-FileUpload-filemovefrom"; function getStringBeforeAtSign(str){ if(_.isEmpty(str)){ return str; } if(str.indexOf("@") != -1){ str = str.substring(0, str.indexOf("@")); } return str; } function getStringAfterAtSign(str){ if(_.isEmpty(str)){ return str; } return (str.indexOf("@") != -1) ? str.substring(str.indexOf("@")) : ""; } function getStringAfterLastSlash(str){ if(!str || (str.indexOf("/") == -1)){ return ""; } return str.substr(str.lastIndexOf("/") + 1); } function getStringBeforeLastSlash(str){ if(!str || (str.indexOf("/") == -1)){ return ""; } return str.substr(0, str.lastIndexOf("/")); } function removeFirstDot(str){ if(str.indexOf(".") != 0){ return str; } return str.substr(1); } function modifyJcrContent(url){ return url.replace(new RegExp("^" + Granite.HTTP.getContextPath()), "") .replace("_jcr_content", "jcr:content"); } function isSelectOne($field) { return !_.isEmpty($field) && ($field.prop("type") === "select-one"); } function setSelectOne($field, value) { var select = $field.closest(".coral-Select").data("select"); if (select) { select.setValue(value); } } function isCheckbox($field) { return !_.isEmpty($field) && ($field.prop("type") === "checkbox"); } function setCheckBox($field, value) { $field.prop("checked", $field.attr("value") === value); } function setWidgetValue($field, value) { if (_.isEmpty($field)) { return; } if (isSelectOne($field)) { setSelectOne($field, value); } else if (isCheckbox($field)) { setCheckBox($field, value); } else { $field.val(value); } } /** * Removes multifield number suffix and returns just the fileRefName * Input: paintingRef-1, Output: paintingRef * * @param fileRefName * @returns {*} */ function getJustName(fileRefName){ if(!fileRefName || (fileRefName.indexOf(SEP_SUFFIX) == -1)){ return fileRefName; } var value = fileRefName.substring(0, fileRefName.lastIndexOf(SEP_SUFFIX)); if(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1 == fileRefName.length){ return value; } return value + fileRefName.substring(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1); } function getMultiFieldNames($multifields){ var mNames = {}, mName; $multifields.each(function (i, multifield) { mName = $(multifield).children("[name$='@Delete']").attr("name"); mName = mName.substring(0, mName.indexOf("@")); mName = mName.substring(2); mNames[mName] = $(multifield); }); return mNames; } function buildMultiField(data, $multifield, mName){ if(_.isEmpty(mName) || _.isEmpty(data)){ return; } _.each(data, function(value, key){ if(key == "jcr:primaryType"){ return; } $multifield.find(".js-coral-Multifield-add").click(); _.each(value, function(fValue, fKey){ if(fKey == "jcr:primaryType" || _.isObject(fValue)){ return; } var $field = $multifield.find("[name='./" + fKey + "']").last(); if(_.isEmpty($field)){ return; } setWidgetValue($field, fValue); }); }); } function buildImageField($multifield, mName){ $multifield.find(".coral-FileUpload:last").each(function () { var $element = $(this), widget = $element.data("fileUpload"), resourceURL = $element.parents("form.cq-dialog").attr("action"), counter = $multifield.find(SEL_FILE_UPLOAD).length; if (!widget) { return; } var fuf = new Granite.FileUploadField(widget, resourceURL); addThumbnail(fuf, mName, counter); }); } function addThumbnail(imageField, mName, counter){ var $element = imageField.widget.$element, $thumbnail = $element.find("." + THUMBNAIL_IMG_CLASS), thumbnailDom; $thumbnail.empty(); $.ajax({ url: imageField.resourceURL + ".2.json", cache: false }).done(handler); function handler(data){ var fName = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileName)), fRef = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileReference)); if(isFileNotFilled(data, counter, fRef)){ return; } var fileName = data[mName][counter][fName], fileRef = data[mName][counter][fRef]; if (!fileRef) { return; } if (imageField._hasImageMimeType()) { imageField._appendThumbnail(fileRef, $thumbnail); } var $fileName = $element.find("[name=\"" + imageField.fieldNames.fileName + "\"]"), $fileRef = $element.find("[name=\"" + imageField.fieldNames.fileReference + "\"]"); $fileRef.val(fileRef); $fileName.val(fileName); } function isFileNotFilled(data, counter, fRef){ return _.isEmpty(data[mName]) || _.isEmpty(data[mName][counter]) || _.isEmpty(data[mName][counter][fRef]) } } //reads multifield data from server, creates the nested composite multifields and fills them function addDataInFields() { $(document).on("dialog-ready", function() { var $multifields = $("[" + DATA_EAEM_NESTED + "]"); if(_.isEmpty($multifields)){ return; } workaroundFileInputPositioning($multifields); var mNames = getMultiFieldNames($multifields), $form = $(".cq-dialog"), actionUrl = $form.attr("action") + ".infinity.json"; $.ajax(actionUrl).done(postProcess); function postProcess(data){ _.each(mNames, function($multifield, mName){ $multifield.on("click", ".js-coral-Multifield-add", function () { buildImageField($multifield, mName); }); buildMultiField(data[mName], $multifield, mName); }); } }); } function workaroundFileInputPositioning($multifields){ //to workaround the .coral-FileUpload-input positioning issue $multifields.find(".js-coral-Multifield-add") .css("position" ,"relative"); } function collectImageFields($form, $fieldSet, counter){ var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){ return $(ele).find(SEL_FILE_UPLOAD).length == 0; }); $fields.each(function (j, field) { var $field = $(field), $widget = $field.find(SEL_FILE_UPLOAD).data("fileUpload"); if(!$widget){ return; } var prefix = $fieldSet.data("name") + "/" + (counter + 1) + "/", $fileRef = $widget.$element.find(SEL_FILE_REFERENCE), refPath = prefix + getJustName($fileRef.attr("name")), $fileName = $widget.$element.find(SEL_FILE_NAME), namePath = prefix + getJustName($fileName.attr("name")), $fileMoveRef = $widget.$element.find(SEL_FILE_MOVEFROM), moveSuffix = $widget.inputElement.attr("name") + "/" + new Date().getTime() + SEP_SUFFIX + $fileName.val(), moveFromPath = moveSuffix + "@MoveFrom"; $('<input />').attr('type', 'hidden').attr('name', refPath) .attr('value', $fileRef.val() || ($form.attr("action") + removeFirstDot(moveSuffix))) .appendTo($form); $('<input />').attr('type', 'hidden').attr('name', namePath) .attr('value', $fileName.val()).appendTo($form); $('<input />').attr('type', 'hidden').attr('name', moveFromPath) .attr('value', modifyJcrContent($fileMoveRef.val())).appendTo($form); $field.remove(); }); } function collectNonImageFields($form, $fieldSet, counter){ var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){ return $(ele).find(SEL_FILE_UPLOAD).length > 0; }); $fields.each(function (j, field) { fillValue($form, $fieldSet.data("name"), $(field).find("[name]"), (counter + 1)); }); } function fillValue($form, fieldSetName, $field, counter){ var name = $field.attr("name"), value; if (!name) { return; } //strip ./ if (name.indexOf("./") == 0) { name = name.substring(2); } value = $field.val(); if (isCheckbox($field)) { value = $field.prop("checked") ? $field.val() : ""; } //remove the field, so that individual values are not POSTed $field.remove(); $('<input />').attr('type', 'hidden') .attr('name', fieldSetName + "/" + counter + "/" + name) .attr('value', value) .appendTo($form); } //collect data from widgets in multifield and POST them to CRX function collectDataFromFields(){ $(document).on("click", ".cq-dialog-submit", function () { var $multifields = $("[" + DATA_EAEM_NESTED + "]"); if(_.isEmpty($multifields)){ return; } var $form = $(this).closest("form.foundation-form"), $fieldSets; $multifields.each(function(i, multifield){ $fieldSets = $(multifield).find("[class='coral-Form-fieldset']"); $fieldSets.each(function (counter, fieldSet) { collectNonImageFields($form, $(fieldSet), counter); collectImageFields($form, $(fieldSet), counter); }); }); }); } function overrideGranite_refreshThumbnail(){ var prototype = Granite.FileUploadField.prototype, ootbFunc = prototype._refreshThumbnail; prototype._refreshThumbnail = function() { var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]"); if (!_.isEmpty($imageMulti)) { return; } return ootbFunc.call(this); } } function overrideGranite_computeFieldNames(){ var prototype = Granite.FileUploadField.prototype, ootbFunc = prototype._computeFieldNames; prototype._computeFieldNames = function(){ ootbFunc.call(this); var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]"); if(_.isEmpty($imageMulti)){ return; } var fieldNames = {}, fileFieldName = $imageMulti.find("input[type=file]").attr("name"), counter = $imageMulti.find(SEL_FILE_UPLOAD).length; _.each(this.fieldNames, function(value, key){ if(value.indexOf("./jcr:") == 0){ fieldNames[key] = value; }else if(key == "tempFileName" || key == "tempFileDelete"){ value = value.substring(0, value.indexOf(".sftmp")) + getStringAfterAtSign(value); fieldNames[key] = fileFieldName + removeFirstDot(getStringBeforeAtSign(value)) + SEP_SUFFIX + counter + ".sftmp" + getStringAfterAtSign(value); }else{ fieldNames[key] = getStringBeforeAtSign(value) + SEP_SUFFIX + counter + getStringAfterAtSign(value); } }); this.fieldNames = fieldNames; this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath); this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath) + removeFirstDot(fieldNames.tempFileName); } } function performOverrides(){ overrideGranite_computeFieldNames(); overrideGranite_refreshThumbnail(); } $(document).ready(function () { addDataInFields(); collectDataFromFields(); }); performOverrides(); })();
When I try to edit the content, the multifield alwais add a new field, do you know why this happening?
ReplyDeleteHi,
DeleteI am facing the same issue in 6.3. Were you able to fix it?
thanks for the article it was really helpful !!!
ReplyDeletequick question, whats determining that the nodes get saved as children, instead of a String array as a property
Hi, have the same doubt, need to store the data as a String array property, how to achieve that ?
ReplyDeleteThanks this was very helpful, and works on 6.3 as well.
ReplyDeleteHi Lopes,
DeleteHave you tried it on 6.2?
Thanks,
Krass
Hi Sreekanth,
ReplyDeleteI have been trying to use the Touch UI Composite Image Multifield with AEM 6.2 SP1 CFP4 & CFP5, but facing the problem where once you open the dialog and click "Add Filed" it shows 3 image areas.
Can you help identify the problem?
Thanks,
Krass
Looks like the issue is related to the index "-1-1" added after the property "productImageRef" - Example: productImageRef-1-1
DeleteI have identified the cause of the issue. Apparently, if the clientlib as defined in the step 2, 3, and 4 of the solution, exists more than once in the repository, that will cause the author dialog of each instance of the component, to have multiple image drop areas equal of the total number of occurrences of the above clientlib. The issue has not only visual effect, but also causes functional issues.
DeleteAfter removing all duplicates from the repository, my Touch UI Composite Image Multifield works fine again.
I hope that helps to all who have faced the same issue.
Krass
HI I am getting same . I did understand your solution. Can you please eloborate.
DeleteHi Srikanth,
ReplyDeleteYour code is really helpful. How can make it as mandatory field. Tried adding property required:true but its not working for image and for other widgets also( text fields and pathbrowser widgets) can you help me on this
Hi Sreekanth,
ReplyDeleteIt looks like code above will work only in case there is just one image field.
Hi Srikanth,
ReplyDeleteYour code is really helpful, but can you please guide me how to have multiple images in multi-field? i am unable to see the first drag & drop area when setting up two fields for images like and
Thanks
Hi All,
ReplyDeleteI have been trying Touch UI Composite Image Multifield with AEM 6.5, but i'm facing the problem where once you open the dialog and click "Add Field" it shows 2 image upload areas.
Can anyone help in this. how to rectify.
Thanks,
Kiruthika