Add Link to Edit Media Asset Fields

There is a way you can be redirected to the media asset that it’s attached to an Image field directly on the content type.

First, we need to create an Editor Descriptor to override the UI behavior how the image field is being rendered. This descriptor will tell all ContentReference fields decorated by UIHint.Image to have that specific EditingClass. Here we are specifying which dojo snippet if going to be used: ImageLink.

[EditorDescriptorRegistration(TargetType = typeof(ContentReference), UIHint = UIHint.Image, EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class ImageLinkDescriptor : ContentReferenceEditorDescriptor<IContentImage>
{
	private readonly FileExtensionsResolver _fileExtensionsResolver;
	public override string RepositoryKey
	{
		get
		{
			return MediaRepositoryDescriptor.RepositoryKey;
		}
	}

	public ImageLinkDescriptor(FileExtensionsResolver fileExtensionsResolver)
	{
		this._fileExtensionsResolver = fileExtensionsResolver;
		base.ClientEditingClass = "epi-cms/widget/ThumbnailSelector";
	}

	public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
	{
		ClientEditingClass = "foundation/editors/ImageLink";

		base.ModifyMetadata(metadata, attributes);
		metadata.EditorConfiguration["allowedExtensions"] = this._fileExtensionsResolver.GetAllowedExtensions(typeof(IContentImage), metadata.Attributes);
	}
}

Our dojo component will basically override the ThumbnailSelector component that comes in the initial install. We are just updating the new template that will be used “dojo/text!./templates/ThumbnailSelector.html”

define("foundation/editors/ImageLink", [
    // dojo
    "dojo/_base/declare",
    "dojo/when",
    "dojo/on",
    "dojo/topic",
    "dojo/dom-class",
    // dijit
    "dijit/focus",
    "dijit/ProgressBar",
    // epi
    "epi/Url",
    "epi/routes",
    "epi/shell/dnd/Target",
    "epi/shell/DialogService",
    // epi-cms
    "epi-cms/core/ContentReference",
    "epi-cms/widget/MediaSelector",
    "epi-cms/contentediting/_ContextualContentContextMixin",
    "epi-cms/command/UploadContent",
    "epi-cms/widget/UploadUtil",
    "epi-cms/widget/viewmodel/MultipleFileUploadViewModel",
    "epi-cms/widget/LocalFolderUploader",
    "dojo/text!./templates/ThumbnailSelector.html",
    "epi/i18n!epi/cms/nls/episerver.cms.widget.thumbnailselector",
    "epi-cms/widget/FilesUploadDropZone"
], function (
    // dojo
    declare,
    when,
    on,
    topic,
    domClass,
    // dijit
    focusManager,
    ProgressBar,
    // epi
    Url,
    routes,
    Target,
    dialogService,
    ContentReference,
    MediaSelector,
    _ContextualContentContextMixin,
    UploadContent,
    UploadUtil,
    MultipleFileUploadViewModel,
    LocalFolderUploader,
    template,
    resources
) {

    var defaultImageUrl = require.toUrl("epi-cms/themes/sleek/images/default-image.png");

    var _dropFileMixin = declare(_ContextualContentContextMixin, {
        // summary:
        //      This mixin is responsible for the local file upload
        // tags:
        //      internal

        // _dialogService: [private] epi/shell/DialogService
        //      The dialog service. Defaults to epi/shell/DialogService.
        _dialogService: null,

        // allowedExtensions: [public] Array
        //      List of file extensions that may be dropped onto the editor
        allowedExtensions: [],

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

            this._dialogService = this._dialogService || dialogService;

            if (this._currentContext.currentMode === "create") {
                return;
            }

            this.uploadCommand = new UploadContent({
                viewModel: this
            });

            when(this.getCurrentContent()).then(function (contentItem) {
                if (this._destroyed) {
                    return;
                }
                this._dropZone.set("settings", {
                    enabled: true,
                    validSelection: true,
                    dropFolderName: this.getContextualRootName(contentItem),
                    descriptionTemplate: resources.dropfile
                });
            }.bind(this));
        },

        _onButtonClick: function () {
            if (this.progressBar.progress && this.progressBar.progress !== this.progressBar.maximum) {
                return;
            }

            this.inherited(arguments);
        },

        upload: function (fileList) {
            var uploader = new LocalFolderUploader({
                model: new MultipleFileUploadViewModel({
                    store: this._store,
                    query: this.query
                }),
                // we decide to not show the thumbnail & progressbar if the first chunk is bigger than 40% of the whole image
                uploadProgressThreshold: 40,
                // show thumbnails on for images smaller than 50 megabytes to avoid hanging the browser by the File API
                thumbnailMaxFileSize: 50 * 1024 * 1024
            });

            uploader.own(on(uploader, "uploadProgress", function (progress) {
                if (this._destroyed) {
                    return;
                }

                this.progressBar.update({ progress: progress });
            }.bind(this)));

            uploader.own(on(uploader, "uploadFilePreview", function (fileBytes, fileName) {
                if (this._destroyed) {
                    return;
                }

                domClass.add(this.domNode, "uploading");
                this.set("selectedContentName", resources.uploading + fileName);
                alert('upload');
                this.set("previewUrl", fileBytes);
                this.thumbnail.src = fileBytes;
            }.bind(this)));

            uploader.own(uploader.on("uploadComplete", function (uploadFiles) {
                if (this._destroyed) {
                    return;
                }

                this.progressBar.update({ progress: 0 });
                focusManager.focus(this.button.domNode); // we need to focus the control so we can trigger the 'onChange' immediately
                domClass.remove(this.domNode, "uploading");

                // handle upload errors, first a general error that causes null to come back from server
                if (!uploadFiles) {
                    this.set("value", this.value);
                    this._dialogService.alert(resources.failed);
                    return;
                }

                // server call was successful however all files were skipped by the user
                if (uploadFiles.length === 0) {
                    return;
                }

                // handle a custom upload error, for example file size exceeded and similar
                if (uploadFiles[0].statusMessage) {
                    this.set("value", this.value);
                    this._dialogService.alert(uploadFiles[0].statusMessage);
                    return;
                }

                this.set("value", uploadFiles[0].contentLink);
                this.onChange(this.value);
                // publish this topic so that the uploaded image is shown in the Assets Pane immediately
                topic.publish("/epi/cms/upload", this.assetsFolderLink);
            }.bind(this)));

            uploader.upload(fileList);
        },

        _refreshTargetUploadContent: function () {
            // We need to handle scenarios when user drops images on a page that does not have assets folder yet.
            // We could try to reuse the logic from ContextualContentForestStoreModel.js & _ContextualContentContextMixin.js
            // The same issue happens in TinyMCE after d&d an image file to a new page that does not have a folder yet.
            var self = this;

            if (this.assetsFolderLink && !this.isPseudoContextualRoot({ contentLink: this.assetsFolderLink })) {
                return this.assetsFolderLink;
            }

            function getAssetsFolderLink(contentItem) {
                if (!self.isPseudoContextualRoot({ contentLink: contentItem.assetsFolderLink })) {
                    return contentItem.assetsFolderLink;
                }

                return self._store.refresh(contentItem.contentLink).then(function (refreshedContent) {
                    return refreshedContent.assetsFolderLink;
                });
            }

            return when(this.getCurrentContent()).then(function (contentItem) {
                return when(getAssetsFolderLink(contentItem)).then(function (contentAssetsLink) {
                    self.assetsFolderLink = new ContentReference(contentAssetsLink).createVersionUnspecificReference().toString();
                    self.query.references = [self.assetsFolderLink];
                    return when(self._store.get(contentAssetsLink)).then(function (contentAssetFolderContent) {
                        self.uploadCommand.set("model", contentAssetFolderContent);
                    });
                });
            });
        },

        _onDrop: function (evt, fileList) {
            // Accept only HTML5 img tag compliant file types, editors can still use the old Assets Pane
            // if they need any other file type like *.svg or *.tiff
            fileList = UploadUtil.filterFileOnly(fileList);
            if (!fileList || fileList.length !== 1) {
                this._dialogService.alert(resources.singleimage);
                return;
            }
            var fileName = fileList[0].name.split(".").pop().toLowerCase();
            if (this.allowedExtensions.indexOf(fileName) === -1) {
                this._dialogService.alert(resources.wrongfileformat);
                return;
            }

            when(this._refreshTargetUploadContent()).then(function () {
                this.uploadCommand.set("fileList", fileList);
                this.uploadCommand.execute();
            }.bind(this));
        }
    });

    return declare([MediaSelector, _dropFileMixin], {
        // summary:
        //      Represents the widget to select ContentReference.
        // tags:
        //      internal

        resources: resources,
        templateString: template,

        _setPreviewUrlAttr: function (value) {
            this.inherited(arguments);
            domClass.toggle(this.displayNode, "dijitHidden", !value);
            domClass.toggle(this.actionsContainer, "dijitHidden", value);
        },

        _getThumbnailUrl: function (content) {
            if (!content.capabilities.generateThumbnail) {
                return content.thumbnailUrl || defaultImageUrl;
            }
            var url = new Url(routes.getActionPath({
                moduleArea: "CMS",
                controller: "Thumbnail",
                action: "Generate"
            }));
            url.query = {
                contentLink: content.contentLink,
                "epi.preventCache": new Date().valueOf()
            };

            this.editButton.style.display = "block";

            //this.editButton.innerHTML = "<a href='#' title='Edit'>Edit</a>";
            //this.editButton.href = url.toString();
            return url.toString();
        },

        _onEditButtonClick: function () {
            var id = this.editButton.getAttribute('data-id');
            var contextParameters = { uri: "epi.cms.contentdata:///" + id, context: this };
            topic.publish("/epi/shell/context/request", contextParameters, { sender: this, forceReload: true });
        },

        _updateDisplayNode: function (content) {
            this.inherited(arguments);
            if (content) {
                this.thumbnail.src = this._getThumbnailUrl(content);
                this.editButton.setAttribute('data-id', content.contentLink);
            } else {
                this.editButton.setAttribute('data-id', null);
            }

            this.stateNode.title = content ? content.name : "";
        },

        postCreate: function () {
            this.inherited(arguments);
            this.query = { query: "getchildren", allLanguages: true };

            this.connect(this.editButton, "onclick", this._onEditButtonClick);
            this.connect(this.clearButton, "onclick", function () {
                this.editButton.style.display = "none";
            }.bind(this));
        },

        _onButtonClick: function () {
            if (this.readOnly) {
                return;
            }

            this.inherited(arguments);
        },

        _setReadOnlyAttr: function (value) {
            this.inherited(arguments);
            this.button.domNode.style.display = "";
            this.button.set("readOnly", value);
        }
    });
});

Finally we have to add the Pencil icon to let the author know that you can edit the media asset by click in the icon.

<div data-dojo-attach-point="inputContainer, stateNode, dropAreaNode" id="widget_${id}"
	 class="dijitReset dijitInline dijitInputContainer epi-resourceInputContainer thumbnail-editor">
	<div class="dijitTextBox" data-dojo-attach-point="_dropZone" data-dojo-type="epi-cms/widget/FilesUploadDropZone" data-dojo-attach-event="onDrop: _onDrop" data-dojo-props="outsideDomNode: this.domNode"></div>
	<a class="thumbnail-button dijitTextBox" href="#" data-dojo-type="dijit/layout/_LayoutWidget" data-dojo-attach-point="button" data-dojo-attach-event="onClick: _onButtonClick">
		<figure data-dojo-attach-point="displayNode" class="dijitHidden">
			<img data-dojo-attach-point="thumbnail" />
			<figcaption class="dijitInline dojoxEllipsis">
				<span data-dojo-attach-point="selectedContentNameNode"></span>
				<span data-dojo-attach-point="selectedContentLinkNode, resourceName"></span>

			</figcaption>
		</figure>
		<div data-dojo-type="dijit/ProgressBar" data-dojo-attach-point="progressBar" data-dojo-props="maximum:100"></div>
		<div data-dojo-attach-point="actionsContainer" class="epi-content-area-actionscontainer">
			<span>${resources.selectimage}</span>
		</div>
	</a>
	<a data-dojo-attach-point="clearButton" href="#" class="epi-clearButton"> </a>
    <a data-dojo-attach-point="editButton" href="#" class="epi-editButton dijitIcon dijitTreeIcon epi-iconPen" title="Edit" style="display: none;"></a>
</div>

If you are wondering how your tree should look like, here is a sample:

And this is should be your module.config:

<?xml version="1.0" encoding="utf-8"?>
<module>
  <dojo>
	<paths>
      <add name="foundation" path="~/ClientResources/Scripts" />
    </paths>
  </dojo>
  ...
</module>

Below is the final result and you will be able to click on the pencil icon and will be redirected to the media asset so you can edit it:

Happy coding 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: