Geta

Menu SE

Content reference selector with direct file upload

Sometimes when developing Episerver solutions you find the need to extend the behaviour of already existing property editors. In this blog post I will briefly describe how I extended the built-in content selector with direct upload functionality using drag and drop. This way you don’t have to create your "Media"-items before being able to select them. You can just upload them directly to the property editor and they will be added to the assets folder of the content you are working on and selected automatically.

Content reference selector with direct upload

Editor descriptor

By overriding the default EditorDescriptor and changing the ClientEditingClass, we can switch out the built-in editor with our own implementation.

public abstract class MediaReferenceEditorDescriptorBase<T> 
: ContentReferenceEditorDescriptor<T> where T: IContentData { protected MediaReferenceEditorDescriptorBase() { this.ClientEditingClass = "mediareference.MediaSelector"; } public override string RepositoryKey { get { return MediaRepositoryDescriptor.RepositoryKey; } } }
[EditorDescriptorRegistration(TargetType = typeof(ContentReference),
EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault, 
UIHint = UIHint.Image)]
public  class ImageReferenceEditorDescriptor: MediaReferenceEditorDescriptorBase<IContentImage>
{
}
[EditorDescriptorRegistration(TargetType = typeof(ContentReference),
EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault, 
UIHint = UIHint.MediaFile)]
public  class MediaReferenceEditorDescriptor: MediaReferenceEditorDescriptorBase<IContentMedia>
{
}

 

Dojo/dijit editor

As you can see below, the editor inherits from the built-in ContentSelector and functionality is then implemented by using existing dojo/dijit components from Episerver and logic for handling the dropzone and uploads directly in the editor.

define([
// dojo
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom-construct",
"dojo/when",
"dojo/promise/all",

// episerver mixins
"epi-cms/_ContentContextMixin",

// episerver shell
"epi/dependency",
"epi/shell/TypeDescriptorManager",

// episerver
"epi-cms/widget/ContentSelector",
"epi-cms/widget/FilesUploadDropZone",
"epi-cms/widget/UploadUtil",
"epi-cms/widget/viewmodel/MultipleFileUploadViewModel",
"epi-cms/widget/MultipleFileUpload",
"epi-cms/ApplicationSettings",
"epi-cms/core/ContentReference",

// template
"dojo/text!epi-cms/widget/templates/AssetsDropZone.html",
"xstyle/css!./style.css",

// Resources
"epi/i18n!epi/cms/nls/episerver.cms.widget.hierachicallist"],
function (

// dojo
array,
declare,
lang,
domConstruct,
when,
promiseAll,

// episerver mixins
_ContentContextMixin,

// episerver shell
dependency,
TypeDescriptorManager,

// episerver
ContentSelector,
DropZone,
UploadUtil,
MultipleFileUploadViewModel,
MultipleFileUpload,
ApplicationSettings,
ContentReference,

//template
dropZoneTemplate,
css,

//resources
res) {
	return declare("mediareference.MediaSelector", [ContentSelector, _ContentContextMixin], {
	res: res,
	droppableContainer: null,
	currentContent: null,
	inCreateMode: false,

	// store related properties
	store: null,
	storeKey: "epi.cms.content.light",
	listQuery: null,

	postMixInProperties: function() {
		this.inherited(arguments);

		this.store = this.store || 
				dependency.resolve("epi.storeregistry").get(this.storeKey);

		// load the current content and context
		dojo.when(
		promiseAll([this.getCurrentContent(), this.getCurrentContext()]), 
		lang.hitch(this, function (result) {
			var content = result[0],
			currentContext = result.length > 1 ? result[1] : null;

			// set the currentcontent
			this.currentContent = content;

			// determine if current content is in "create"-mode
			this.inCreateMode = currentContext && 
			currentContext.currentMode && 
			currentContext.currentMode === "create";
		}));
	},

	buildRendering: function() {
		this.inherited(arguments);
		this._setupDropZone();
	},

	_setupDropZone: function() {
		this.inherited(arguments);

		// don't create the dropzone if content is in "create"-mode 
		// (since assets folder is not created yet)
		if (this.inCreateMode) {
			return;
		}

		// create a container for the additional dropzone (mainly for css reasons)
		this.droppableContainer = domConstruct.create("div",
			{ className: "custom-dropzone" }
		);

		// use the same html-template and resources that is used in the ordinary
		// hierarchical list
		this.own(this._dropZone = new DropZone({
			templateString: dropZoneTemplate,
			res: res,
			outsideDomNode: this.droppableContainer
		}));
		domConstruct.place(this._dropZone.domNode, this.droppableContainer, "last");

		// create event listener for the onDrop-event
		this.connect(this._dropZone, "onDrop", this._onDrop);

		// append the container
		this.domNode.appendChild(this.droppableContainer);

	},

	_onDrop: function(evt, fileList) {
		// filters out empty files and files without type
		fileList = UploadUtil.filterFileOnly(fileList);

		// ignore empty filelists (folders)
		if (!fileList || fileList.length <= 0) {
			return;
		}

		// upload the file, make sure to only upload the first one since we 
		// only support one file at a time
		this.upload(fileList[0], this.currentContent.contentLink, true);
	},

	upload: function(file, targetId, createAsLocalAsset) {
		var uploader = new MultipleFileUpload({
			model: new MultipleFileUploadViewModel({})
		});

		// event triggered when the upload is completed
		uploader.on("uploadComplete", lang.hitch(this, function(uploadedFiles) {
			if (!uploadedFiles || uploadedFiles.length <= 0) {
				return;
			}

			var uploaded = uploadedFiles[0];

			// if a file has been uploaded to a newly created
 			// assetsfolder, 
			// we need to refresh the content to get the correct assetsfolder link.
			var currentContentWithoutVersion =
				new ContentReference(this.currentContent.contentLink)
				.createVersionUnspecificReference().toString();

			dojo.when(this.store.refresh(currentContentWithoutVersion),
			 lang.hitch(this,function (refreshedContent) {
				
				this.currentContent = refreshedContent;

				// used for refreshing the treeview and also find the droppedfile
				// set the list query to be able to query children of the current 
				// assets folder (needed to find out the contentLink 
				// for the uploaded item)
				this.listQuery = {
					referenceId: refreshedContent.assetsFolderLink,
					query: "getchildren",
					allLanguages: true,
					typeIdentifiers: []
				};

				// query the store for the children to assetsfolder
				when(this.store.query(this.listQuery), lang.hitch(this,
					function (contentList) {
						// filter out only files with same 
						// name as uploaded file
						var createdContent = array.filter(
						contentList, function (content) {
							return content.name == uploaded.fileName;
						});

						if (!createdContent || 
							createdContent.length <= 0) {
							return;
						}

						// select first match
						var created = createdContent[0];

						// only set the value if the uploaded 
						// type is allowed
						if
						(this._isContentAllowed(created.typeIdentifier)){
							this._setValueAttr(created.contentLink);
						}
					}
				));
			}));
		}));

		// upload the file to the assets folder
		uploader.set("uploadDirectory", targetId);
		uploader.set("createAsLocalAsset", createAsLocalAsset);
		uploader.upload([file]);
	},

	_isContentAllowed: function(contentTypeIdentifier) {
		var acceptedTypes = TypeDescriptorManager.getValidAcceptedTypes(
			[contentTypeIdentifier],
			this.allowedTypes,
			this.restrictedTypes);

		return !!acceptedTypes.length;
	}
});
});

 

Stylesheet

Since the editor is using the same dropzone template as what is used by Episerver in the assets pane, we need to do some small layout changes to make it look good in the property editor.

.custom-dropzone {
	margin: 16px 0 0 0;
	padding: 0 5px 0 0;
}


.custom-dropzone .epi-assetsDropZone {
	border: 3px solid #C0C0C0;
	border-style: dashed;
	height: 90px;
	display: table;
	position: relative;
}

.custom-dropzone .epi-assetsDropZone.epi-dragOver {
	border: 3px solid #9FC733;
	border-style: dashed;
}


Known issues / limitations

  • If a file with the same name as the file being uploaded already exists in the assets folder, the first one returned from getchildren-query on the assets folder will be selected.
  • If multiple files are dropped, all files will be uploaded to the assets folder, the first one will be selected.
  • Dropped folders will be ignored

There might still be some issues or bugs with the current implementation, these are the ones I know of so far. I will update this blog post with full source code later. We are planning to release this as a Nuget package as well.

comments powered by Disqus