Geta

Meny NO

Filtered display options menu based on content type in EPiServer

A thing that has bothered me for a long time with EPiServer is that you, out of the box, can't have different display options for different blocks. Based on the design for your site you might want to force one block to full width and another to half width etc. You can do this with CSS but what's bothering me is that the editor is presented with an option that doesn't make any sense. So, I decided to fix it. :)

This blog post is kind of part 2 of my earlier post How to limit rendering to specific display options and set default display option for a block. I end it with the following question: "Is it possible to disable the display option select item in the context menu if no template is found to support it? Please let me know. It bugs me a lot.".

Since nobody answered my question I just had to figure it out myself. So, here it is. Maybe it's not the most pretty solution but it works.

Specification

I have these display options defined for my site:

For a given block I only want support for Full and Half width so I want the menu filtered into this:

The display option tags is defined in code like this:

public static class DisplayOptions
{
    public const string FullWidth = "FullWidth";
    public const string ThreeFourthsWidth = "ThreeFourthsWidth";
    public const string TwoThirdsWidth = "TwoThirdsWidth";
    public const string HalfWidth = "HalfWidth";
    public const string OneThirdWidth = "OneThirdWidth";
    public const string OneFourthWidth = "OneFourthWidth";
}

 

Server side code

ISpecialRenderingContent interface

I need a way to set the supported display options for a content type so I created this interface to implement.

public interface ISpecialRenderingContent
{
    string[] SupportedDisplayOptions { get; }
}

 

Block example

[ContentType(DisplayName = "My full and half width block", GUID = "some-guid")]
public class MyBlock : BlockData, ISpecialRenderingContent
{
    public virtual string Heading { get; set; }
    public virtual XhtmlString MainBody { get; set; }

    public string[] SupportedDisplayOptions
    {
        get
        {
            return new[]
            {
                DisplayOptions.FullWidth,
                DisplayOptions.HalfWidth
            };
        }
    }
}

 

RestStore - SupportedDisplayOptionsStore

I need some way to get the supported display options for a given ContentReference from my client script code (I'll get back to that). I decided to create a simple RestStore for this.

[RestStore("supporteddisplayoptions")]
public class SupportedDisplayOptionsStore : RestControllerBase
{
    private readonly IContentLoader _contentLoader;
    private readonly DisplayOptions _displayOptions;

    public SupportedDisplayOptionsStore(IContentLoader contentLoader, 
                                        DisplayOptions displayOptions)
    {
        if (contentLoader == null) throw new ArgumentNullException("contentLoader");
        if (displayOptions == null) throw new ArgumentNullException("displayOptions");
        _contentLoader = contentLoader;
        _displayOptions = displayOptions;
    }

    [HttpGet]
    public RestResultBase Get(string id)
    {
        ContentReference contentLink;

        if (!ContentReference.TryParse(id, out contentLink))
        {
            return Default();
        }

        IContent content;

        if (!_contentLoader.TryGet(contentLink, out content))
        {
            return Default();
        }

        var specialRenderingContent = content as ISpecialRenderingContent;

        if (specialRenderingContent != null)
        {
            var supportedDisplayOptions = 
_displayOptions
.Where(x => specialRenderingContent.SupportedDisplayOptions.Contains(x.Tag)); return Rest(supportedDisplayOptions.Select(x => x.Id)); } // Default to all display options. return Default(); } private RestResultBase Default() { return Rest(_displayOptions.Select(x => x.Id)); } }

 

Client side code

~/ClientResources/Scripts/ModuleInitializer.js

To register the rest store I created a module initializer.

define([
    "dojo",
    "dojo/_base/declare",
    "epi/_Module",
    "epi/dependency",
    "epi/routes"
], function (
    dojo,
    declare,
    _Module,
    dependency,
    routes
) {
    return declare("app.ModuleInitializer", [_Module], {

        initialize: function () {

            this.inherited(arguments);
            var registry = this.resolveDependency("epi.storeregistry");

            //Register the store
            registry.create("supporteddisplayoptions", this._getRestPath("supporteddisplayoptions"));
        },

        _getRestPath: function (name) {
            return routes.getRestPath({ moduleArea: "app", storeName: name });
        }
    });
});

 

~/ClientResources/Scripts/CacheManager.js

I don't want to call the rest store every time an editor selects a display option so I created a simple manager to cache them on the client side.

define([
    "dojo/_base/array",
    "dojo/_base/declare",
    "dojo/_base/lang"
], function(
    array,
    declare,
    lang
) {

    return declare("app.CacheManager", null, {
        _data: null,
        _cacheLength: null,
        _length: null,

        constructor: function (args) {
            this._data = {};
            this._cacheLength = 50;
            this._length = 0;
        },

        add: function(key, value) {
            if (this._length > this._cacheLength) {
                this.flush();
            }

            if (!this._data[key]) {
                this._length++;
            }

            this._data[key] = value;
        },

        load: function(key) {
            if (this._length < 1) {
                return null;
            }

            if (this._data[key]) {
                return this._data[key];
            }

            return null;
        },

        flush: function() {
            this._data = {};
            this._length = 0;
        }
    });
});

 

~/ClientResources/Scripts/widget/DisplayOptionSelector.js

This is the "kind of" ugly part of this. I had to extract the built-in display option selector code from EPiServer and modify it to my needs. The namespace and class name is important and shouldn't be modified.

define("epi-cms/widget/DisplayOptionSelector", [
    "dojo/_base/array",
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/when",

    "dijit/MenuSeparator",

    "epi/dependency",

    "epi-cms/widget/SelectorMenuBase",

    // Resouces
    "epi/i18n!epi/cms/nls/episerver.cms.contentediting.editors.contentarea.displayoptions",

    // Widgets used in template
    "epi/shell/widget/RadioMenuItem",

    "app/CacheManager"
], function (
    array,
    declare,
    lang,
    when,

    MenuSeparator,

    dependency,

    SelectorMenuBase,

    // Resouces
    resources,

    RadioMenuItem,

    CacheManager
) {

    return declare([SelectorMenuBase], {
        // summary:
        //      Used for selecting display options for a block in a content area
        //
        // tags:
        //      internal

        // model: [public] epi-cms.contentediting.viewmodel.ContentBlockViewModel
        //      View model for the selector
        model: null,

        // _resources: [private] Object
        //      Resource object used in the template
        headingText: resources.title,

        _rdAutomatic: null,
        _separator: null,

        // Custom properties for hacked implementation.
        supportedDisplayOptions: null,
        _cacheManager: null,

        // Adapted from EPiServer and slightly modified.
        // Added initialization of cache manager.
        postCreate: function () {
            // summary:
            //      Create the selector template and query for display options

            this.inherited(arguments);

            this.own(this._rdAutomatic = new RadioMenuItem({
                label: resources.automatic,
                value: ""
            }));

            this.addChild(this._rdAutomatic);
            this._rdAutomatic.on("change", lang.hitch(this, this._restoreDefault));

            this.own(this._separator = new MenuSeparator({ baseClass: "epi-menuSeparator" }));
            this.addChild(this._separator);

            this._cacheManager = this._getCacheManager();
        },

        // Adapted from EPiServer. No modifications.
        _restoreDefault: function () {
            this.model.modify(function () {
                this.model.set("displayOption", null);
            }, this);
        },

        // Adapted from EPiServer. No modifications.
        _setModelAttr: function (model) {
            this._set("model", model);

            this._setup();
        },

        // Adapted from EPiServer. No modifications.
        _setDisplayOptionsAttr: function (displayOptions) {
            this._set("displayOptions", displayOptions);

            this._setup();
        },

        // Adapted from EPiServer and modified.
        // Called on load and when a display option is selected.
        _setup: function () {
            if (!this.model || !this.displayOptions) {
                return;
            }

            //Destroy the old menu items
            this._removeMenuItems();

            var selectedDisplayOption = this.model.get("displayOption");

            if (this.supportedDisplayOptions == null) {
                this._setSupportedDisplayOptions(selectedDisplayOption);
            } else {
                this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
            }
        },

        // Custom method. Called in _setup method. Calls the rest store to
        // get supported display options. The result is cached.
        _setSupportedDisplayOptions: function (selectedDisplayOption) {
            var cacheKey = this._getCacheKey();
            var cachedData = this._cacheManager.load(cacheKey);

            if (cachedData != null) {
                this.supportedDisplayOptions = cachedData;
                this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
                return;
            }

            var storeRegistry = dependency.resolve("epi.storeregistry");
            var store = storeRegistry.get("supporteddisplayoptions");

            when(store.get(this.model.contentLink), lang.hitch(this, function (supportedDisplayOptions) {
                this.supportedDisplayOptions = array.filter(this.displayOptions, function (displayOption) {
                    return array.indexOf(supportedDisplayOptions, displayOption.id) > -1;
                });

                // Add to cache
                this._cacheManager.add(this._getCacheKey(), this.supportedDisplayOptions);

                // Create menu items.
                this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
            }), lang.hitch(this, function (err) {
                // An error occured. Fallback to unfiltered/standard display options.
                this.supportedDisplayOptions = this.displayOptions;

                // Create menu items.
                this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
            }));
        },

        // Extracted from the original _setup method.
        _setMenuItems: function (displayOptions, selectedDisplayOption) {
            array.forEach(displayOptions, function (displayOption) {
                var item = new RadioMenuItem({
                    label: displayOption.name,
                    iconClass: displayOption.iconClass,
                    displayOptionId: displayOption.id,
                    checked: selectedDisplayOption === displayOption.id,
                    title: displayOption.description
                });

                this.own(item.watch("checked", lang.hitch(this, function (property, oldValue, newValue) {
                    if (!newValue) {
                        return;
                    }
                    // Modify the model
                    this.model.modify(function () {
                        this.model.set("displayOption", displayOption.id);
                    }, this);
                })));

                this.addChild(item);
            }, this);

            this._rdAutomatic.set("checked", !selectedDisplayOption);
        },

        // Adapted from EPiServer. No modifications.
        _removeMenuItems: function () {
            var items = this.getChildren();
            items.forEach(function (item) {
                if (item === this._rdAutomatic || item == this._separator) {
                    return;
                }
                this.removeChild(item);
                item.destroy();
            }, this);
        },

        // Custom method. Gets the global cache manager.
        _getCacheManager: function () {
            window.supportedDisplayOptionsCache = window.supportedDisplayOptionsCache || new CacheManager();
            return window.supportedDisplayOptionsCache;
        },

        // Custom method. Gets the supported display options cache key for current content.
        _getCacheKey: function() {
            return "SupportedDisplayOptions-" + this.model.contentLink;
        }
    });
});

 

Wrapping it up

For all this to work you need to update/create module.config with this code:

<?xml version="1.0" encoding="utf-8"?>
<module clientResourceRelativePath="" loadFromBin="false">
   <assemblies>
      <!-- The assembly containing the rest store needs to be added. -->
      <add assembly="MyAssemblyName" />
   </assemblies>
   <dojo>
      <paths>
         <add name="app" path="ClientResources/Scripts" />
      </paths>
   </dojo>
   <clientModule initializer="app.ModuleInitializer">
      <moduleDependencies>
         <add dependency="CMS" type="RunAfter" />
      </moduleDependencies>
   </clientModule>
   <clientResources>
      <!-- Inject our custom Display Option selector -->
      <add name="epi-cms.widgets.base"
              path="~/ClientResources/Scripts/widget/DisplayOptionSelector.js"
              resourceType="Script"
      />
   </clientResources>
</module>

 

kommentarer drevet av Disqus