Kontakt oss
Tilbake til bloggen
Development

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.

Content reference selector with direct file upload
Article image

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.

Snakk med oss

Har du et prosjekt vi kan hjelpe med?

Ta en uforpliktende prat med teamet vårt om hvordan vi kan løse din utfordring.

Ta kontakt

Hold deg oppdatert på fremtidens e-handel

Ukentlige analyser, trender og praktiske råd — kun det vi selv ville lest.