In this article I’d like to show a custom implementation of image property. There are already few blog posts and code snippets about improved image editor, but most of them focus on showing thumbnail for selected image. In my code I refactored image selection dialog and replaced content tree with media component. It can be useful for Editors, because media component has many features that can improve their work.
Standard image property
Episerver has built-in image property. To use it you should add UIHint.Image uihint on the property. Otherwise default ContentReference property will be used. When selecting an image, dialog window with content tree is displayed. This is the same tree as for ContentReference property, but filtered by images. The tree is useful for well structured media assets, but for folders with a lot of images inside there are two problems:
- It doesn’t support paging – all children for selected folder are loaded in one request (and also all thumbnails)
- You don’t see parent page for all the time – it’s hard to follow the structure.
Also you can’t do all of the things that you can do in media component, like download the image or upload new image, create new folder, etc.
Image property with media component dialog
The Media component (available on the right side in edit mode) is divided into two sections. First section displays folders in a tree structure. Second is a flat list of images located under selected folder.
Component has vertical orientation, so the main change that I had to made was to display images list next to the folder structure:
Now new editor with media component dialog looks like:
Pagining
When directory contains many images then initially first items 25 are loaded. Rest will be queried when scrolling the list:
Other features
Additionally Editors will get access to all actions from image context menu, like download or edit.
They can upload new images:
Or drop images directly from local computer to the property:
The only disadvantage comparing to the default property is that selected image is not preselected in the dialog window. Only parent folder of the image is selected. This is because of the infinite scroll. We don’t won’t to load to many images at start.
Adding image property to existing solution
I have no nuget for this extension. It’s just three files with few lines of code that has to be copied to the existing solution: EditorDescriptor, client widget and styles.
In the Editor I had to:
- initialize media component
- turn off multiselect which is available for media component and should not be for image property
- enable OK button when image is selected
- preselect parent directory when editing the image
- set dialog title
Nothing more. Whole logic is in the media component. I’m just reading selected value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
define([ "dojo/_base/declare", "dojo/string", "epi/dependency", "epi/shell/TypeDescriptorManager", "epi/shell/widget/dialog/Dialog", "epi-cms/widget/ContentSelector", "epi-cms/component/Media", "epi/i18n!epi/cms/nls/episerver.cms.widget.contentselector", "xstyle/css!./editor.css" ], function ( declare, stringUtil, dependency, TypeDescriptorManager, Dialog, ContentSelector, Media, localization ) { return declare([ContentSelector], { postMixInProperties: function () { this.inherited(arguments); this._contentDataStore = this.contentDataStore || dependency.resolve("epi.storeregistry").get("epi.cms.contentdata"); }, _getDialog: function () { if (this.dialog && this.dialog.domNode) { return this.dialog; } // set dialog title based on allowed types var title = localization.title; if (this.allowedTypes && this.allowedTypes.length === 1) { var name = TypeDescriptorManager.getResourceValue(this.allowedTypes[0], "name"); if (name) { title = stringUtil.substitute(localization.format, [name]); } } // handle allowed types in media component - show only images instead of all media files var containedTypes = this.allowedTypes.slice(); containedTypes.push("episerver.core.contentfolder"); var modelClassName = declare([Media.prototype.modelClassName], { _setupTreeStoreModel: function () { this.containedTypes = containedTypes; this.inherited(arguments); } }); this.mediaComponent = new Media({ column: 0, componentId: "e333279b1b82451e9f13850b8fdaafd4", lastOpenHeight: null, open: true, repositoryKey: this.repositoryKey, root: "1", modelClassName: modelClassName }); this.mediaComponent.model._saveTreePaths(null); this.mediaComponent.list.set("selectionMode", "single"); this.mediaComponent.searchResultList.set("selectionMode", "single"); this.own( this.mediaComponent.model.selection.watch("data", function (name, oldValue, newValue) { if (!newValue || newValue.length !== 1) { this.dialog.definitionConsumer.setItemProperty(this.dialog._okButtonName, "disabled", true); return; } var content = newValue[0].data; if (content.contentTypeName === "Content Folder") { //TODO: hack this.dialog.definitionConsumer.setItemProperty(this.dialog._okButtonName, "disabled", true); return; } this._setDialogButtonState(content.contentLink); }.bind(this)) ); // setup initial value this.contentSelectorDialog = { set: function (name, value) { if (name === "value") { if (value) { this.mediaComponent.model.treeStoreModel.getAncestors(value, function (ancestors) { this.mediaComponent.model.set("treePaths", [ancestors]); }.bind(this)); } else { this.mediaComponent.set("treePaths", null); } } }.bind(this) }; this.dialog = new Dialog({ title: title, dialogClass: "alloy-image-dialog epi-dialog-portrait epi-dialog-confirm epi-dialog--wide", content: this.mediaComponent }); this.dialog.own(this.mediaComponent); this.connect(this.dialog, "onExecute", "_onDialogExecute"); this.connect(this.dialog, "onHide", "_onDialogHide"); this.connect(this.dialog, "onShow", "_onShow"); return this.dialog; }, _onDialogExecute: function () { var value = this.mediaComponent.model.selection.data[0].data.contentLink; this._setValueAndFireOnChange(value); } }); }); |
Unfortunately styles doesn’t look nice. That because most of styles in media component are inline. To override them I had to use “!important” almost everywhere :(.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
.alloy-image-dialog .epi-hierarchicalList [data-dojo-attach-point="container"] { height: 379px; } .alloy-image-dialog .epi-hierarchicalList .dijitTree { width: 300px !important; height: 378px !important; border-right: 1px solid #bbb; } .alloy-image-dialog .epi-hierarchicalList .epi-gadgetBottomContainer { top: 0 !important; left: 302px !important; width: 418px !important; height: 360px; } .alloy-image-dialog .epi-hierarchicalList [data-dojo-attach-point="createContentArea"] { width: 418px !important; } .alloy-image-dialog .epi-hierarchicalList .dijitSplitter { display: none; } .alloy-image-dialog .epi-hierarchicalList [data-dojo-attach-point="listContainer"] .epi-thumbnailContentList { width: 418px !important; } .alloy-image-dialog .epi-hierarchicalList [data-dojo-attach-point="listContainer"] .epi-contentList { width: 418px !important; height: 360px !important; } .alloy-image-dialog .epi-contentList [data-dojo-attach-point="listContainer"] .epi-thumbnailContentList { height: 337px !important; } .alloy-image-dialog .epi-assetsDropZone { height: 450px; } |
And then EditirDescriptor file. It’s only responsibility is to set client editing class to “alloy/editors/assets/Editor“.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Collections.Generic; using EPiServer.Cms.Shell.UI.ObjectEditing.EditorDescriptors; using EPiServer.Core; using EPiServer.Shell.ObjectEditing.EditorDescriptors; using EPiServer.Shell.ObjectEditing; namespace AlloyTemplates.Business.EditorDescriptors { [EditorDescriptorRegistration(TargetType = typeof(ContentReference), UIHint = UIHint)] public class AssetsEditorDescriptor : ImageReferenceEditorDescriptor { public const string UIHint = "Assets"; public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes) { base.ModifyMetadata(metadata, attributes); ClientEditingClass = "alloy/editors/assets/Editor"; } } } |
To use new property add “Assets” UIHint on the ContentReference property:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System.ComponentModel.DataAnnotations; using AlloyTemplates.Business.EditorDescriptors; using EPiServer.Core; using EPiServer.DataAbstraction; using EPiServer.DataAnnotations; using EPiServer.SpecializedProperties; using AlloyTemplates.Models.Blocks; [ContentType(GUID = "19671657-B684-4D95-A61F-8DD4FE60D559"] public class StartPage : SitePageData { [UIHint(AssetsEditorDescriptor.UIHint)] public virtual ContentReference HeroImage { get; set; } // ... other properties } |
Here you can download full source code.