A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Anders Hattestad
Nov 26, 2013
  5046
(0 votes)

File selector dojo property for EPiServer 7

Have made myself a dojo property for selection images. Noting fancy, but I though I should share some of my findings when creating a dojo property. There is some examples out there, but another one can’t hurt.

The basic concept here is that you can drag a file into an area. And I display a thumbnail of the file.

image Drag the file into the area
image Show a thumbnail
image After saving, show delete and all files from folder
image Select all files from folder
image after saving so a list of all the files. The property contains a href to all current files in that folder so IReference is set

Since I wanted to select more than one file, I created a normal property and tagged my property with that and a UIHint

Code Snippet
  1. [BackingType(typeof(PropertyFiles))]
  2. [CultureSpecific]
  3. [Display(Order = 1)]
  4. [UIHint("Files")]
  5. public virtual string FileList { get; set; }

Then I created a EditorDescriptor, where I points to my JavaScript file

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using EPiServer.Shell.ObjectEditing.EditorDescriptors;
  4.  
  5. namespace EPiServer.Templates.Alloy.Business.EditorDescriptors
  6. {
  7.     [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "Files")]
  8.     public class FilesEditorDescriptor : EditorDescriptor
  9.     {
  10.         public override void ModifyMetadata(Shell.ObjectEditing.ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
  11.         {
  12.             ClientEditingClass = "itera.editors.FilesList";
  13.  
  14.             base.ModifyMetadata(metadata, attributes);
  15.         }
  16.     }
  17. }

When I created the JavaScript file, I got some strange findings. Some examples out there do stuff in postCreate with the value, but as far as I can tell if there are more than one property being edited the value in postCreate is null.

So one should do stuff in the _setValue: function (value, updateTextarea) {…} it can seems. On an other property I use we update an iframe. If you do that you must do that last in the _setValue function, since we lose focus to the function, and never returns.

I added the Source and Target classes to handle drag and drop. But I got problem when having more than one target to drag items into. maybe a bug, or maybe some JavaScript code error on my part. (Guess my part Smilefjes )

I also didn’t manage to drag folders into the area. Have you got that to work? please let me know.

The code displays the current value of the property inside the targetDisplay, and if I add a new item I create a new div that I adds to the display. the div contains a data-href attribute with the selected file. I also have a text area with the same html code. That is maybe redundant. One should probably only use the html value of the targetDisplay as the master for the value. This property is used for files, so I display a small image. I have a File system in place /FileCache/ that resize the images based on the added filename (here height_70.height_70.mode_crop.jpg) So you need to change that to your resizing method.

Code Snippet
  1. define([
  2.     "dojo/_base/array",
  3.     "dojo/_base/connect",
  4.     "dojo/_base/declare",
  5.     "dojo/_base/lang",
  6.  
  7.     "dijit/_CssStateMixin",
  8.     "dijit/_Widget",
  9.     "dijit/_TemplatedMixin",
  10.     "dijit/_WidgetsInTemplateMixin",
  11.  
  12.     "dijit/form/Textarea",
  13.  
  14.     "epi/epi",
  15.     "epi/shell/widget/_ValueRequiredMixin",
  16.         "epi/shell/dnd/Source",
  17.     "epi/shell/dnd/Target"
  18. ],
  19. function (
  20.     array,
  21.     connect,
  22.     declare,
  23.     lang,
  24.  
  25.     _CssStateMixin,
  26.     _Widget,
  27.     _TemplatedMixin,
  28.     _WidgetsInTemplateMixin,
  29.  
  30.     Textarea,
  31.     epi,
  32.     _ValueRequiredMixin,
  33.     Source,
  34.     Target
  35. ) {
  36.  
  37.     return declare("itera.editors.FileList", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin], {
  38.  
  39.         templateString: "<div class=\"dijitInline ownerDiv\">\\par                 <div class=\"epi-content-area-editor\" >\\par                     <div dojoAttachPoint=\"target\" class=\"epi-content-area-actionscontainer files\">Drag files</div>\\par                     <div style=\"clear:both;\"></div>\\par                     <div dojoAttachPoint=\"targetDisplay\"  ></div>\\par                     <div style=\"clear:both;\"></div>\\par                 </div>\\par                 <div style=\"clear:both;\"></div>\\par                             <input type=text value=\"\" class='actionField' style=\"display:none;\" data-dojo-attach-point=\"actions\" data-dojo-attach-event=\"click:_onAction\"/>\\par                             <div data-dojo-attach-point=\"stateNode, tooltipNode\">\\par                                 <div data-dojo-attach-point=\"textArea\" data-dojo-type=\"dijit.form.Textarea\"  style=\"width:200px;display:none;\"></div>\\par                             </div>\\par                             <br />\\par                         </div>",
  40.  
  41.         baseClass: "epiStringList",
  42.  
  43.       
  44.         intermediateChanges: false,
  45.  
  46.         value: null,
  47.  
  48.         multiple: true,
  49.  
  50.         onChange: function (value) {
  51.             // Event
  52.         },
  53.  
  54.         postCreate: function () {
  55.             // call base implementation
  56.             this.inherited(arguments);
  57.  
  58.             // Init textarea and bind event
  59.             this.textArea.set("intermediateChanges", this.intermediateChanges);
  60.             this.connect(this.textArea, "onChange", this._onTextAreaChanged);
  61.             this._setupTarget();
  62.         },
  63.         _onAction: function () {
  64.             console.log("_onAction= " + this.targetDisplay.innerHTML);
  65.             this.textArea.value = this.targetDisplay.innerHTML;
  66.             this._setValue(this.targetDisplay.innerHTML, false);
  67.         },
  68.         _setupTarget: function () {
  69.             var target = new Target(this.target, {
  70.                 accept: ["fileurl"],
  71.                 //Set createItemOnDrop if you're only interested to receive the data, and not create a new node.
  72.                 createItemOnDrop: true
  73.             });
  74.  
  75.             this.connect(target, "onDropData", "_onDropDataFile");
  76.  
  77.             //var targetFolders = new Target(this.targetFolders, {
  78.             //    accept: ["link", "fileurl", "FM-FileLink"],
  79.             //    //Set createItemOnDrop if you're only interested to receive the data, and not create a new node.
  80.             //    createItemOnDrop: true
  81.             //});
  82.             //this.connect(targetFolders, "onDropData", "_onDropDataFolder");
  83.         },
  84.         _drawItems:function(list) {
  85.             this.targetDisplay.innerHTML = list;
  86.            
  87.         },
  88.         _onDropData: function (path, source) {
  89.            
  90.             var value = path;
  91.             if (isFolder)
  92.                 value = path.substr(0, path.lastIndexOf("/")+1);
  93.             var isFolder = false;
  94.             if (source == this.targetFolders)
  95.                 isFolder = true;
  96.             if (source.parent == this.targetFolders)
  97.                 isFolder = true;
  98.  
  99.             console.log(path + "=" + value + " " + source+" "+isFolder);
  100.             var list = this.value;
  101.             if (typeof this.value === "string") {
  102.                 // Split list
  103.                 list = this._stringToList(this.value);
  104.  
  105.             } else if (!this.value) {
  106.                 // use empty array for empty value
  107.                 list = [];
  108.             }
  109.  
  110.  
  111.             var txt = list.join("");
  112.           
  113.             var stringCheck = "/";
  114.             if (txt.indexOf('data-href="' + value + '"') == -1) {
  115.                 var foundIt = (value.lastIndexOf(stringCheck) === value.length - stringCheck.length) > 0;
  116.                 if (foundIt)
  117.                     txt += '<div style="float:left;border:1px solid black;padding-right:5px;" data-href="' + value + '">All files from folder:<br />' + value + '</div>';
  118.                 else
  119.                     txt += '<div style="float:left;border:1px solid black;padding-right:5px;" data-href="' + value + '"><img src="/FileCache' + value + '/height_70.height_70.mode_crop.jpg" style="width:70px;height:70px;" title="' + value + '" /></div>';
  120.  
  121.             }
  122.             this._setValue(txt, true);
  123.         },
  124.         _onDropDataFolder: function (dndData, source, nodes, copy) {
  125.  
  126.             //Drop item is an array with dragged items. This example just handles the first item.
  127.             var dropItem = dndData ? (dndData.length ? dndData[0] : dndData) : null;
  128.             console.log(dropItem + " " + dndData);
  129.             if (dropItem) {
  130.  
  131.                 //The data property might be a deffered so we need to call dojo.when just in case.
  132.                 dojo.when(dropItem.data, dojo.hitch(this, function (value) {
  133.                     //Do something with the data, here we just log it to the console.
  134.                     this._onDropData(value, source);
  135.                     
  136.  
  137.  
  138.                 }));
  139.             }
  140.  
  141.  
  142.         },
  143.         _onDropDataFile: function (dndData, source, nodes, copy) {
  144.          
  145.             //Drop item is an array with dragged items. This example just handles the first item.
  146.             var dropItem = dndData ? (dndData.length ? dndData[0] : dndData) : null;
  147.             console.log(dropItem + " " + dndData);
  148.             if (dropItem) {
  149.  
  150.                 //The data property might be a deffered so we need to call dojo.when just in case.
  151.                 dojo.when(dropItem.data, dojo.hitch(this, function (value) {
  152.                     //Do something with the data, here we just log it to the console.
  153.                     this._onDropData(value, source);
  154.                    
  155.                    
  156.                 }));
  157.             }
  158.            
  159.           
  160.         },
  161.        
  162.         isValid: function () {
  163.             return true;
  164.         },
  165.  
  166.         // Setter for value property
  167.         _setValueAttr: function (value) {
  168.             this._setValue(value, true);
  169.         },
  170.  
  171.         _setReadOnlyAttr: function (value) {
  172.             this._set("readOnly", value);
  173.             this.textArea.set("readOnly", value);
  174.         },
  175.  
  176.         // Setter for intermediateChanges
  177.         _setIntermediateChangesAttr: function (value) {
  178.             this.textArea.set("intermediateChanges", value);
  179.             this._set("intermediateChanges", value);
  180.         },
  181.  
  182.         // Event handler for textarea
  183.         _onTextAreaChanged: function (value) {
  184.             this._setValue(value, false);
  185.         },
  186.  
  187.         _setValue: function (value, updateTextarea) {
  188.             // Assume value is an array
  189.             var list = value;
  190.             if (typeof value === "string") {
  191.                 // Split list
  192.                 list = this._stringToList(value);
  193.  
  194.             } else if (!value) {
  195.                 // use empty array for empty value
  196.                 list = [];
  197.             }
  198.  
  199.             if (this._started && epi.areEqual(this.value, list)) {
  200.                 return;
  201.             }
  202.             var txt = list.join("");
  203.             if (txt == "")
  204.                 this._set("value", null);
  205.             else
  206.                 this._set("value", txt);
  207.            
  208.             updateTextarea && this.textArea.set("value", txt);
  209.             this._drawItems(txt);
  210.             if (this._started && this.validate()) {
  211.                 // Trigger change event
  212.                 this.onChange(list);
  213.             }
  214.  
  215.         },
  216.  
  217.         // Convert a string to a list
  218.         _stringToList: function (value) {
  219.  
  220.             // Return empty array for
  221.             if (!value || typeof value !== "string") {
  222.                 return [];
  223.             }
  224.  
  225.             // Trim whitespace at start and end
  226.             var trimmed = value.replace(/^\s+|\s+$/g, "");
  227.  
  228.             // Trim whitespace around each linebreak
  229.             var trimmedLines = trimmed.replace(/(\s*\n+\s*)/g, "\n");
  230.  
  231.             // Split into list
  232.             var list = trimmedLines.split("\n");
  233.  
  234.             return list;
  235.         }
  236.  
  237.  
  238.     });
  239. });

After the property is saved  have changed the Value in my PropertyFiles property. I find all the div.'s with the data-href attribute and creates a new html value with the div and some checkboxes to delete or select all files in that folder. I also add <a href to the files in the end of the html so the IReference will be set, and editors will get warning if they try to delete a file in use by this property.

To communicate to the dojo property I use this line of JavaScript

var obj=$(this).closest('.ownerDiv').find('.actionField')

this line will get a hold of the textbox in the dojo property

<input type=text value=\"\" class='actionField'  data-dojo-attach-point=\"actions\" data-dojo-attach-event=\"click:_onAction\" style=\"display:none;\"/>\

If i then do obj.click() the _onAction will be triggered. I don't use the value of the textbox here, since i only hide the div, and remove the data-href attribute, but the pattern is nice for using that value for something Smilefjes

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using EPiServer.Core;
  6. using EPiServer.Web.PropertyControls;
  7. using System.Web.UI.WebControls;
  8. using EPiServer.PlugIn;
  9. using EPiServer.Web.Hosting;
  10. using HtmlAgilityPack;
  11. using System.IO;
  12. using EPiServer;
  13. using System.Web.Hosting;
  14.  
  15. namespace Itera.Models.Properties
  16. {
  17.     [PropertyDefinitionTypePlugIn]
  18.     [System.Serializable]
  19.     public class PropertyFiles : PropertyLongString
  20.     {
  21.          public override object Value
  22.         {
  23.             get
  24.             {
  25.                 return base.Value;
  26.             }
  27.             set
  28.             {
  29.                 var str = value as string;
  30.                 if (str != null)
  31.                 {
  32.                     var doc = new HtmlDocument();
  33.                     doc.LoadHtml(str);
  34.                     var refLinks = "";
  35.                     foreach (var item in doc.DocumentNode.ChildNodes)
  36.                     {
  37.                         if (item.Name == "div" && item.Attributes.Contains("data-href") && item.Attributes["data-href"].Value!="")
  38.                         {
  39.                             var href = item.Attributes["data-href"].Value;
  40.                             var inner = "";
  41.                             refLinks += "<a href=\"" + href + "\" class=\"ignore\" style=\"display:none;\">" + href + "</a>";
  42.                             item.Attributes.Remove("style");
  43.                            
  44.  
  45.                             if (href.EndsWith("/"))
  46.                             {
  47.                                 item.Attributes.Add("style", "border:1px solid black;margin-bottom:5px;");
  48.  
  49.                                 inner = "<div>";
  50.                                 inner += "<input type=checkbox onclick=\"$(this).parent().parent().removeAttr('data-href');$(this).parent().parent().css('display','none');var obj=$(this).closest('.ownerDiv').find('.actionField');obj.val('" + href + "');obj.click();\" />delete";
  51.  
  52.                                 inner += "</div>";
  53.                                 var dir = System.Web.Hosting.HostingEnvironment.VirtualPathProvider.GetDirectory(href) as UnifiedDirectory;
  54.                                 if (dir != null)
  55.                                 {
  56.                                     inner += "All files from folder:<br />" + dir.VirtualPath;
  57.                                     foreach (UnifiedFile fil2 in dir.Files)
  58.                                     {
  59.                                       
  60.                                         if (BaseMedia.IsMedia(fil2.VirtualPath))
  61.                                         {
  62.                                             refLinks += "<a href=\"" + fil2.VirtualPath + "\" class=\"ignore\" style=\"display:none;\">" + fil2.VirtualPath + "</a>";
  63.                                             inner += "<div style=\"float:left;\">";
  64.                                             inner += "<img style=\"width:80px;height:80px;\"  src=\"/FileCache" + fil2.VirtualPath + "/width_80.height_80.mode_crop.jpg\" data-org=\"" + fil2.VirtualPath + "\" />";
  65.                                             inner += "</div>";
  66.                                         }
  67.                                     }
  68.                                 }
  69.                             }
  70.                             else
  71.                             {
  72.                                 item.Attributes.Add("style", "border:1px solid black;float:left;margin-right:5px;margin-bottom:5px;");
  73.                                 var filname = Path.GetFileName(href);
  74.                                 var path = href.Substring(0, href.Length - filname.Length);
  75.                                 inner = "<div>";
  76.                                 inner += "<input type=checkbox onclick=\"$(this).parent().parent().removeAttr('data-href');$(this).parent().parent().css('display','none');var obj=$(this).closest('.ownerDiv').find('.actionField');obj.val('" + href + "');obj.click();\" />delete";
  77.  
  78.                                 inner += "<input type=checkbox onclick=\"$(this).parent().parent().attr('data-href','" + path + "');var obj=$(this).closest('.ownerDiv').find('.actionField');$(this).parent().html('All files from folder:<br />" + path + "');obj.val('" + href + "');obj.click();\" />all<br/>";
  79.                                 
  80.  
  81.                                 
  82.                                 item.Attributes.Remove("onclick");
  83.                                 
  84.                                 inner += "<img style=\"width:80px;height:80px;\" src=\"/FileCache" + href + "/width_80.height_80.mode_crop.jpg\" data-org=\"" + href + "\" />";
  85.                                 inner += "</div>";
  86.                             }
  87.  
  88.                             item.InnerHtml = inner+"<div style=\"clear:both;\"></div>";
  89.                        
  90.                         }
  91.                     }
  92.                     base.Value = doc.DocumentNode.OuterHtml;
  93.                 } else {
  94.                 base.Value = value;
  95.                 }
  96.             }
  97.         }
  98.  
  99.  
  100.          public List<VirtualFile> GetFiles()
  101.          {
  102.              var result = new List<VirtualFile>();
  103.              var str = this.LongString;
  104.              if (str != null)
  105.              {
  106.                  var doc = new HtmlDocument();
  107.                  doc.LoadHtml(str);
  108.  
  109.                  foreach (var item in doc.DocumentNode.ChildNodes)
  110.                  {
  111.                      if (item.Name == "div" && item.Attributes.Contains("data-href") && item.Attributes["data-href"].Value != "")
  112.                      {
  113.                          var href = item.Attributes["data-href"].Value;
  114.  
  115.                          if (href.EndsWith("/"))
  116.                          {
  117.  
  118.                              var dir = System.Web.Hosting.HostingEnvironment.VirtualPathProvider.GetDirectory(href) as UnifiedDirectory;
  119.                              if (dir != null)
  120.                              {
  121.                                  foreach (UnifiedFile fil2 in dir.Files)
  122.                                  {
  123.                                          result.Add(fil2);
  124.                                  }
  125.                              }
  126.                          }
  127.                          else
  128.                          {
  129.                              var file = System.Web.Hosting.HostingEnvironment.VirtualPathProvider.GetFile(href) ;
  130.                              result.Add(file);
  131.                          }
  132.  
  133.  
  134.  
  135.                      }
  136.                  }
  137.  
  138.              }
  139.              return result;
  140.          }
  141.  
  142.     }
  143.    
  144. }

I guess I could have done this more dojo Smilefjes, but with my current knowledge this was the best I managed. If some of you have made a more advanced dojo property in EPiServer 7 please make a blog so we can learn from you.

Nov 26, 2013

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026

How to run Optimizely CMS on VS Code Dev Containers

VS Code Dev Containers is an extension that allows you to use a Docker container as a full-featured development environment. Instead of installing...

Daniel Halse | Jan 30, 2026

A day in the life of an Optimizely OMVP: Introducing Optimizely Graph Learning Centre Beta: Master GraphQL for Content Delivery

GraphQL is transforming how developers query and deliver content from Optimizely CMS. But let's be honest—there's a learning curve. Between...

Graham Carr | Jan 30, 2026