Visualizing block layout in on-page edit mode

The Blocks feature was introduced in episerver version 7 and replaced the Composer feature previously available. Blocks matched to some degree the functionality of Composer. The technologies were similar enough to allow for an automatic migration from Composer modules to Episerver blocks, although some manual steps were required. This migration tool migrates Composer modules to blocks, Composer version 4R2 to episerver 7.1.

Being technically similar, the proposed usage of the two features were different. With Composer, one was encouraged to freely build the structure of the page using modules. There were modules for layout and modules for content. It was the responsibility of the editor to “compose” a page by combining modules of different types. The use of blocks on the other hand, as a layout component organizing other blocks, is not advertised. The alloy site has no such layout building block and that type of block is difficult to work with in episerver on-page edit mode, where blocks-in-blocks are neither direct editable nor visually outlined, although rendered. To edit a block, you have to click through every level in the blocks hierarchy.

There are of course reasons for discouraging the use of a deep module/block layout, the difficulties accomplishing responsive design being one of them. But what if your site is migrated from a composer rich one, or that you for some reason prefer a design with a deep level of blocks?

In this post, a gadget that aids the editor working with a site containing deeply leveled blocks is presented. An episerver gadget extending the edit mode is also called a widget and is written with dojo’s UI library Dijit. The widget serves two purposes. First, it speeds up editing blocks by making it possible to reach the edit page for every block with just one click. Second, it gives the editor a good overview of the structure of the blocks in a page.

All the blocks in the current page being edited are listed to the left. The indentation shows the hierarchy. Hovering a row in the widget highlights the corresponding block in the editing area. Clicking a row in the widget results in a navigation to the edit page for that block.

Getting started with dojo

The user interface for editing content in Episerver is build using the dojo framework. To learn about dojo and the pluggable components called widgets, I recommend reading the basics at dojotoolkit.org, study this widget by Grzegorz Wiecheć and spend some time reading source code in the CMS module, modules/_protected/CMS.zip. Episerver widgets are located in folder CMS.zip\[version]\ClientResources\epi-cms\widget in a documented and uncompressed format. Note that the installed version of the dojo framework, which can be of interest when considering certain features, is revealed by pasting doco.version into the browsers F12-console in episerver edit mode.

A simple widget pluggable into the Episerver edit UI

The widget is visible below the page navigation tree

The widget can be added to the visual studio solution in different ways but by adding it to the modules\_protected path we benefit from the configuration made for the built-in edit mode modules in Episerver. When creating a new Episerver project in Visual Studio using the episerver extension, either choosing Alloy or Empty project, the modules\_protected path will be configured for containing edit mode gadgets.

The file structure (except server side registration component)

 

module.config


<?xml version="1.0" encoding="utf-8" ?>
<module>
  <dojo>
    <paths>
      <add name="myEditModeWidget" path="scripts" />
    </paths>
  </dojo>
</module>

myEditModeGadget.js


define([
    "dojo/_base/declare",
    "dijit/_WidgetBase"
], function (
    declare,
    _WidgetBase
) {
    return declare([_WidgetBase], {
        //startup is one of the lifecycle methods in a widget.
        startup: function () {
            this.inherited(arguments);
            console.log('I am started.');
        }
    });
});

The default configuration requires the widget to be registered in web.config


  <episerver.shell>
    <publicModules rootPath="~/modules/" autoDiscovery="Modules" />
    <protectedModules rootPath="~/EPiServer/">      
      <add name="myEditModeWidget" />

Finally, a server side registration of the gadget


using EPiServer.Shell.ViewComposition;

namespace BlockFinder.BlockFinderComponent
{
    [Component]
    public class MyEditModeWidgetComponent : ComponentDefinitionBase
    {
        public MyEditModeWidgetComponent()
            : base("myEditModeWidget/myEditModeWidgetComponent")
        {
            Description = "The description of this gadget";
            Title = "myEditModeGadget";

            this.Categories = new string[] { "content" };
            this.PlugInAreas = new string[]
            {
                "/episerver/cms/mainnavigation"
            };
        }
    }
}

Widget interaction

A widget should extend dijit/_WidgetBase which define a number of callbacks that controls the lifecycle of the widget. It is optional to override the methods but typically one of callbacks, of which most controls different phases of the initialization process, is overridden to set up the widget.

The pluggable widgets communicate with the UI framework of the episerver edit mode and with each other using a publish and subscribe message-mechanism provided by dojo. A widget can use this feature directly or by inheriting classes that offer callbacks triggered in response to various messages. E.g. the class epi/shell/_ContextMixin abstracts away the messages and provides a callback that notifies when context changes (i.e. editable content) filtering out duplicate messages etc. The class also provide a public method for querying current context.

Only a few messages are “public” and documented but it is possible to watch all the published messages in real time. By pasting the following code snippet into the F12-console of the browser, the names of every published message will be written out. The snippet does not display any message arguments but could easily be extended to do so. Any number of arguments could be specified.


require(['dojo/aspect', 'dojo/topic'], function (aspect, topic) {
    aspect.after(topic, 'publish', function (topicName) {
        console.log('topicName=' + topicName);
    }, true);
});

The publish/subscribe feature does not provide a function for querying the available messages but by using the dojo/aspect module it is possible to add functionality to existing methods. The “after” function above “inserts” an anonymous function to be executed every time the publish method of the topic object is executed.

Three messages of special interest, listed below, shows up when selecting a page in the page tree. The first two happens right before the content is replaced and the last one occurs when the new content is rendered.


  topicName=/epi/shell/context/request
  topicName=/epi/shell/context/changed
  topicName=dijit__TemplatedMixin_4-addChild
  

Episerver on page edit

In order to keep the markup rendered in on page edit mode (OPE) as close as possible to the markup shown to the visitor, the Episerver framework renders the page in its own iframe and places the OPE specific markup in a separate element on the top of the iframe, in a so-called overlay. Some minimal content is however added to the iframe so that the editable contents can be found by the framework. A block is identified by its id in a data-attribute, data-epi-block-id. The name of the block can be found in the attribute data-epi-content-name.


<iframe name="sitePreview">
  ...
  <div data-epi-block-id="195" data-epi-content-name="Article">
    block content here...
  </div>
  ...
</iframe>

Also blocks buried deep down in the DOM have their corresponding data-attributes rendered, not just the blocks that are selectable in the current OPE view.

Block Finder

The purpose of the first widget was to show the widget infrastructure. A more interesting widget and reason for this post, BlockFinder, is using the techniques described above. The source code files of BlockFinder follow the same structure as the files of the first widget.

Editors and admins have access to protected modules . There is also a server side source file not shown in this image.

The widget is registered at the server side with the component attribute.


using EPiServer.Shell.ViewComposition;

namespace BlockFinder.BlockFinderComponent
{
    [Component]
    public class BlockFinderComponent : ComponentDefinitionBase
    {
        public BlockFinderComponent()
            : base("blockFinder/blockFinderComponent")
        {
            Description = "Find blocks in a page";
            Title = "Block Finder";

            this.Categories = new string[] { "content" };
            this.PlugInAreas = new string[]
            {
                "/episerver/cms/mainnavigation"
            };
        }
    }
}

The widget script, blockFinderComponent.js, is built using the dojo framework and plain javascript.


define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dojo/store/Memory",
  "dojo/on",
  "dgrid/Keyboard", //capture mouse events
  "dgrid/OnDemandGrid",
  "dgrid/Selection",
  "dijit/layout/_LayoutWidget",
  "dojo/topic"
],
function (
  declare,
  lang,
  Memory,
  on,
  Keyboard,
  OnDemandGrid,
  Selection,
  _LayoutWidget,
  topic
) {
  return declare([_LayoutWidget], {

    //css class for the containing element of the widget
    baseClass: "blockFinder", 

    _gridClass: declare([OnDemandGrid, Selection, Keyboard]),

    _savedBackground: {},

    constructor: function () {
      this.store = new Memory({ data: {} });
    },

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

      var gridSettings = {
        columns: this._createGridColumns(),
        store: this.store,
        selectionMode: "single"
      };

      this.grid = new this._gridClass(gridSettings, this.domNode);
    },

    startup: function () {
      if (this._started) {
        return;
      }

      this.inherited(arguments);
      this.grid.on(".dgrid-row:click", lang.hitch(this, this._changeContext));
      this.grid.on(".dgrid-row:mouseover", lang.hitch(this, this._highLightBlock));
      this.grid.on(".dgrid-row:mouseout", lang.hitch(this, this._highLightBlockOff));

      //handles here will get destroyed because _LayoutWidget is based on dijit/_WidgetBase that is based on dijit/Destroyable where the own function is defined.
      this.own(
        topic.subscribe("dijit__TemplatedMixin_2-addChild", lang.hitch(this, this._findBlocks)),
        topic.subscribe("dijit__TemplatedMixin_3-addChild", lang.hitch(this, this._findBlocks)),
        topic.subscribe("dijit__TemplatedMixin_4-addChild", lang.hitch(this, this._findBlocks)),
        topic.subscribe("dijit__TemplatedMixin_5-addChild", lang.hitch(this, this._findBlocks)),
        topic.subscribe("dijit__TemplatedMixin_6-addChild", lang.hitch(this, this._findBlocks))
      );
    },

    //called by epi when gadget is removed
    destroy: function () {
      if (this.grid) {
        this.grid.destroy();
      }
      this.inherited(arguments);
    },

    _highLightBlock: function (e) {
      var row = this.grid.row(e);
      if (row && row.data && row.data.elem) {
        var node = row.data.elem;
        this._savedBackground[row.data.id] = node.style["background-color"];
        node.style["background-color"] = "#C6D501";
      }
    },

    _highLightBlockOff: function (e) {
      var row = this.grid.row(e);
      if (row && row.data && row.data.elem) {
        var node = row.data.elem;
        node.style["background-color"] = this._savedBackground[row.data.id];
      }
    },

    _changeContext: function (e) {
      var row = this.grid.row(e);
      var pageLink = row.data.blockid; 
      var contextParameters = { uri: 'epi.cms.contentdata:///' + pageLink };
      topic.publish("/epi/shell/context/request", contextParameters);
    },

    //content loaded and ready to be queried
    _findBlocks: function () {
      //get a reference to the iframe were the content is rendered
      var ifrm = document.getElementsByName("sitePreview")[0].contentWindow.document;
      this.store.setData([]);//empty store
      this._addBlocksToStore(ifrm, "0", 0);
      this.grid.refresh();
    },

    //traverses the iframe for blocks
    _addBlocksToStore: function (node, id, depth) {
      var blockid = node.getAttribute !== undefined ? node.getAttribute("data-epi-block-id") : null;
      if (blockid !== null) {
        var row = { id: id, blockid: blockid, name: node.getAttribute("data-epi-content-name"), elem: node, depth: depth };
        this.store.add(row);
        depth++;
      }
      for (var i = 0; i < node.childNodes.length; i++) {
        this._addBlocksToStore(node.childNodes[i], id + "" + i, depth);
      }
    },

    _createGridColumns: function () {
      var columns = {
        name: {
          label: 'Block Name',
          className: "epi-width50",
          renderCell: function (object, value, node, options) {
            node.innerHTML = "<div style='margin-left: " + (object['depth'] * 1) + "em'>" + object['name'] + "</div>";
          },
        }
      };
      return columns;
    }
  });//declare
});//define

Some minimal styling in blockFinder.css.


.blockFinder .dgrid-row:hover {
    background-color: #C0C0C0;
}
.blockFinder .dgrid-row {
    padding: 0px;
}

module.config declares the resources of the add-on.


<?xml version="1.0" encoding="utf-8" ?>
<module>
  <dojo>
    <paths>
      <add name="blockFinder" path="scripts" />
    </paths>
  </dojo>
  <clientResources>
    <add name="epi-cms.widgets.base" path="scripts/content/blockFinder.css" resourceType="Style">
    </add>
  </clientResources>
</module>

Using the module/gadget configuration found in the Alloy site as well as in the empty site of the episerver visual studio plugin, a module installed in the protected area has to be listed in web.config like this:


    <protectedModules rootPath="~/EPiServer/">
      <add name="blockFinder" />

In another post I will describe how to assemble this widget into a nuget package and distribute it.