Workaround for extending the LinkItemCollection to support anchors

PLACE FOR BLOG

Workaround for extending the LinkItemCollection to support anchors

Workaround for extending the LinkItemCollection to support anchors

marija

As announced in one of the previous posts on supporting adding links with anchors by EPiServer editors, other than extending a TinyMCE link, one might also want to have the same functionality when adding links using a LinkItemCollection or Url.

When previous experience doesn't help

Unfortunately, the knowledge I've gained by creating the new TinyMCE button for creating a link didn't help me much with achieving the same goal with LinkItemCollection. Apart from that, once I've realized that I would need to do the same for the following types: PageReference, Url and LinkItemCollection, it was quite clear I needed a different approach.

New approach

So, instead of trying to override EPiServer components, I've created** a block that contains a Url property and anchor on page property**, which are interdependent. Here is a blogpost that helped me get started with the idea, written by Duong from EPiServer. It might be wise to read the intro and the first part - "All Properties mode overview", for better understanding of group layout containers.

Editors guide

For editors, the process is as follows:

  1. Click "create a block here" inside a content area
  2. Choose LinkWithAnchorSharedBlock (hopefully with a friendlier name :D)
  3. Select a page
  4. Choose an anchor from the dropdown (if needed)

Implementation

Since the example included local blocks, I needed to create two blocks - one block that serves as a property grouper and the other one containing this block. There might be a way to skip this part, but I didn't dig deeper into EPi components to search for the wrapping layout container, there might be a way to have only one shared block.

LinkWithAnchor local block:

    [ContentType(
        GUID = "56F9FD92-1013-44A3-A7D4-66647FB54C6A",
        AvailableInEditMode = false
    )]
    public class LinkWithAnchorLocalBlock : SiteBlockData
    {
        [Display(
            GroupName = SystemTabNames.Content,
            Order = 30)]
        [Required]
        public virtual string Caption { get; set; }

        [Display(
            GroupName = SystemTabNames.Content,
            Order = 40)]
        [BackingType(typeof(PropertyUrl))]
        public virtual Url Info { get; set; }

        [Display(
            GroupName = SystemTabNames.Content,
            Order = 50)]
        [UIHint(SiteUiHints.AnchorsOnPage)]
        public virtual string AnchorOnPage { get; set; }
    }

LinkWithAnchor shared block:

    [ContentType(
        GUID = "4F58A959-4485-400F-8AB1-B78206028581"
    )]
    public class LinkWithAnchorSharedBlock : SiteBlockData
    {
        [Display(
            GroupName = SystemTabNames.Content,
            Order = 200
        )]
        public virtual LinkWithAnchorLocalBlock LocalBlock { get; set; }
    }

Do note that LinkWithAnchorLocalBlock has AvailableInEditMode = false, hence, it cannot be created as a shared block from the block assets panel. AnchorsOnPage editor descriptor has been mentioned in the previous blogs, but for the reference, it can be found here (doesn't do much, it only sets the width):

    [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "AnchorsOnPage")]
    public class AnchorsOnPageEditorDescriptor : EditorDescriptor
    {
        public AnchorsOnPageEditorDescriptor()
        {
            SelectionFactoryType = typeof(PropertySettingsSelectionFactory);
            ClientEditingClass = "epi-cms.contentediting.editors.SelectionEditor";
            EditorConfiguration.Add("style", "width: 240px");
        }
    }

Other files you might need

Here you can see and download the controller, viewmodel and view as well as a LinkDto:

Controller

    public class LinkWithAnchorSharedBlockController : BlockControllerBase
    {
        public override ActionResult Index(LinkWithAnchorSharedBlock currentBlock)
        {
            var page = currentBlock.LocalBlock.Info.ToPage();
            var anchorOnPage = !string.IsNullOrEmpty(currentBlock.LocalBlock.AnchorOnPage) ?
                string.Format("#{0}", currentBlock.LocalBlock.AnchorOnPage) : null;

            var viewModel = new LinkWithAnchorSharedBlockViewModel(currentBlock)
            {
                Link = page != null ?
                    new LinkDto
                    {
                        Url = string.Format("{0}{1}", page.ToFriendlyUrl(), anchorOnPage),
                        Caption = currentBlock.LocalBlock.Caption
                    } : null
            };

            return PartialView("Blocks/LinkWithAnchorSharedBlock", viewModel);
        }
    }

ViewModel

    public class LinkWithAnchorSharedBlockViewModel : BlockViewModel
    {
        public LinkWithAnchorSharedBlockViewModel(LinkWithAnchorSharedBlock currentBlock)
            : base(currentBlock)
        {

        }

        public LinkDto Link { get; set; }

        public bool ShowLink 
        {
            get
            {
                return Link != null;
            }
        }
    }

View

@model PROJECT.Web.Models.ViewModels.LinkWithAnchorSharedBlockViewModel

@if (Model.ShowLink)
{
    Html.RenderPartial("DisplayTemplates/LinkDto", Model.Link);
}
else if (Model.CurrentBlock.LocalBlock.Info != null)
{
    @Model.CurrentBlock.LocalBlock.Caption
}

LinkDto

    public class LinkDto
    {
        public string Url { get; set; }
        public string Caption { get; set; }
        public string Title { get; set; }
        public int PageId { get; set; }
        public bool IsExternalLink { get; set; }
        public string Target { get; set; }
        public string TargetAttribute
        {
            get
            {
                return string.IsNullOrWhiteSpace(Target)
                       ? string.Empty
                       : string.Format("target=\"{0}\"", Target);
            }
        }
        public PageShortcutType ShortcutType { get; set; }
    }

Now, the fun part!

Rewriting the container to update anchor items once a page is selected

The interesting part is the method addChild! It is called prior to adding each property as a child inside the container.

So, what do you need to do inthere - if your child is a URL, you need to hook to its change event. And voila! It should work. But it doesn't yet, since on first load of All-properties view, the value of the previously set URL is not set. So this does work, but only when the page is selected/changed in the selection dialog.

So, we need to trigger the on change manually:

if (!options.infoUrl.value) {
    options.infoUrl.value = {
        href: options.infoUrl.params.value,
        name: "",
    target: "",
        text: ""
    };
}
if (!options.anchorOnPage.value) {
    options.anchorOnPage.value = options.anchorOnPage.params.value;
}
// call the method that executes after onchange event
this._pageSelected();

Now, the functionality is in place. The whole js can be found here:

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/dom-style",
    "dojo/_base/xhr",

    "epi/shell/layout/SimpleContainer"
],
function (
    declare,
    lang,
    domStyle,
    xhr,

    LinkWithAnchorLocalBlockContainer
) {
    var options = {
        infoUrl: null,
        anchorOnPage: null,

        addChild: function(child) {
            this.inherited(arguments);

            // when All-properties view is opened, child.name is prefixed with local block property
            if (child.name == "localBlock.info") {
                options.infoUrl = child;

                // Connect to change and drop events to update the anchors dropdown
                this.own(options.infoUrl.on("change", lang.hitch(this, this._pageSelected)));
                
            } else if (child.name == "localBlock.anchorOnPage") {
                options.anchorOnPage = child;
                
                // change event for previously set URL doesn't get triggered on first load of editview
                // so the dropdown population and value selection didn't work

                // this is a workaround of triggering it manually
                // however, the value is not set at the time of execution of addChild, 
                // so we need to set it manually and trigger _pageSelected();
                if (!options.infoUrl.value) {
                    options.infoUrl.value = {
                        href: options.infoUrl.params.value,
                        name: "",
                        target: "",
                        text: ""
                    };
                }
                if (!options.anchorOnPage.value) {
                    options.anchorOnPage.value = options.anchorOnPage.params.value;
                }
                this._pageSelected();
            }
        },

        _pageSelected: function() {
            var url = options.infoUrl.value;
            
            if (!url || !url.href) {
                var emptyAnchorsOnPage = Array();
                emptyAnchorsOnPage.push({
                    text: "",
                    value: ""
                });
                this._populateAnchorOnPageDropdown(emptyAnchorsOnPage);
            } else {
                var href = url.href;
                var anchorApiUrl = '/api/tiny/anchors/fromurl/?url=' + href;

                xhr.get({
                    url: anchorApiUrl,
                    handleAs: "json",
                    load: function(data) {
                        if (!data || data.length == 0) {
                            return;
                        }
                        var allAnchorsOnPage = Array();
                        allAnchorsOnPage.push({
                            text: "",
                            value: ""
                        });
                        for (var i = 0; i  0) {
                options.anchorOnPage.set("value", currentAnchorValue);
            }
            domStyle.set(options.anchorOnPage.domNode, {
                display: "block"
            });
        }
    };

    return declare([LinkWithAnchorLocalBlockContainer], options);
});

Update: after upgrade to 7.7, the functionality stopped working. Below is an updated file. I haven't tested it on 7.6, so it might also be needed for that version.

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/dom-style",
    "dojo/_base/xhr",

    "epi/shell/layout/SimpleContainer"
],
function (
    declare,
    lang,
    domStyle,
    xhr,

    LinkWithAnchorLocalBlockContainer
) {
    var options = {
        infoUrl: null,
        anchorOnPage: null,

        addChild: function(child) {
            this.inherited(arguments);

            // when All-properties view is opened, child.name is prefixed with local block property name
            if (child.name == "localBlock.info") {
                options.infoUrl = child;

                // Connect to change and drop events to update the anchors dropdown
                this.own(options.infoUrl.on("change", lang.hitch(this, this._pageSelected)));
                
            } else if (child.name == "localBlock.anchorOnPage") {
                options.anchorOnPage = child;
                
                // change event for previously set URL doesn't get triggered on first load of editview
                // so the dropdown population and value selection didn't work

                // this is a workaround of triggering it manually
                // however, the value is not set at the time of execution of addChild, 
                // so we need to set it manually and trigger _pageSelected();
                if (!options.infoUrl.value) {
                    options.infoUrl.value = options.infoUrl.params.value;
                }
                if (!options.anchorOnPage.value) {
                    options.anchorOnPage.value = options.anchorOnPage.params.value;
                }
                this._pageSelected();
            }
        },

        _pageSelected: function() {
            var url = options.infoUrl.value;
            var anchorValue = options.anchorOnPage.value;
            if (!url) {
                var emptyAnchorsOnPage = Array();
                emptyAnchorsOnPage.push({
                    text: "",
                    value: ""
                });
                this._populateAnchorOnPageDropdown(emptyAnchorsOnPage);
            } else {
                var anchorApiUrl = '/api/tiny/anchors/fromurl/?url=' + url;

                xhr.get({
                    url: anchorApiUrl,
                    handleAs: "json",
                    load: function(data) {
                        if (!data || data.length == 0) {
                            return;
                        }
                        var allAnchorsOnPage = Array();
                        allAnchorsOnPage.push({
                            text: "",
                            value: ""
                        });
                        for (var i = 0; i  0) {
                options.anchorOnPage.set("value", currentAnchorValue);
            }
            domStyle.set(options.anchorOnPage.domNode, {
                display: "block"
            });
        }
    };

    return declare([LinkWithAnchorLocalBlockContainer], options);
});

Known limitations

There are two things that don't work at this point:

  1. I haven't managed to enable calling of _pageSelected on drag and drop event, so the page needs to be selected exclusively from the Link selector.
  2. Edit-on-page doesn't work well with this property (since the options.infoUrl.params.value is undefined, so the event can't be triggered), so edit-on-page is not supported and the editor needs to select all-properties view.

LEAVE A COMMENT