November Happy Hour will be moved to Thursday December 5th.

Anders Hattestad
Nov 26, 2013
  8015
(5 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

Joshua Folkerts
Joshua Folkerts Nov 28, 2013 03:10 AM

I have to say, you are more bold than I am. I just gave up trying to make custom properties EPiServer 7 with Dojo. So much to do for such a simple development task in EPiServer 6r2. Nice work Anders.

Martin Pickering
Martin Pickering Nov 28, 2013 11:29 AM

Here, here.
You just know that when you receive the feedback "really, I have to do all that? For something quite so simple?" that you've got the design wrong.
@EPiServer, listen up guys - if you have Implementers saying that they are avoiding making particular types of enhancements and extensions due to the integration points requiring "too much heavy lifting" and being ever so brittle then you need to step in and sort out the fundamental issues associated with the Use Case.

Nov 29, 2013 10:21 AM

I agree with previous comments this is excellent. Thanks for sharing!
I haven't done any custom properties myself yet because frankly the task feels a bit daunting.

Arild Henrichsen
Arild Henrichsen Nov 29, 2013 02:33 PM

Of the 16 blog posts on World mentioning "dojo", only 2 are from non-EPiServer employees. So either all devs understand the dojo/dijit paradigm perfectly and feel no need to blog about it - OR very few devs have a good grasp of it. Kudos to Anders for deep diving into this.

Martin Pickering
Martin Pickering Dec 2, 2013 06:16 PM

My deepest and humblest of apologies to Anders for not having said the first time around...

You are a BRAVE, if not fool hardy individual, for trying - well done and thanks ever so much for sharing

:)

Nicklas Ekered
Nicklas Ekered Dec 10, 2013 09:02 AM

Nice work!

Although EPiServer should have recognized the potential horror in development time, for example as Joshua mentions it´s really not efficient when it comes to coding simpler things and perhaps more advanced as well!?

Erik Täck
Erik Täck Dec 10, 2013 05:08 PM

I will buy a beer (or a non-alcoholic beverage of similar worth) to the person that create a wrapper so one can use a simpler javascript framework to create editor properties, like jquery or vanilla.js instead of this complexity-over-usefulness framework.

Dec 17, 2013 01:47 PM

Kudos for hitting that dojo, bru :)

Matti Mertojoki
Matti Mertojoki Dec 18, 2013 12:22 PM

I will buy two more beers to the java wrapper programmer.. These Dojo solutions are just pain in a butt.

Anders Hattestad
Anders Hattestad Jan 13, 2014 12:26 PM

Read this blog if you get the deadlock error..
http://world.episerver.com/Blogs/Anders-Hattestad/Dates/2014/1/Error-Deadlock-risk-detected/

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog