4

I have an item type called footnote. Whenever a content editor links to an item of this type I want a data attribute to be set instead of setting an href. The data attribute will contain the html that will be used to populate a modal that appears when the link is clicked. I know there are plenty of ways to go about doing this but is there a "best way" to do this? I was planning on adding a step in the renderField pipeline before the ExpandLinks process that would set the appropriate data attributes (can be seen here: Adding Attributes to Rendered Link Programmatically).

A second option would be to create a custom Sitecore.Pipelines.HttpRequest.DynamicLinkResolver that will have to use a custom Sitecore.Links.DynamicLink object. I do want this to work for both rich text fields and link fields.

A third option would be to create a custom field renderer that handles this.

This should work for both link fields and rich text fields.

  • Are the links in a Rich Text Editor, or do you just mean link fields? – Zachary Kniebel Jun 19 '17 at 21:23
  • @ZacharyKniebel good question. I want it to work with link fields and rich text fields. I'll add that requirement to my question. – Teeknow Jun 20 '17 at 12:59
3

From the way that the issue is described in your post, the actual links that you are trying to intercept are a bit ambiguous. If your links are all from link fields, then the answer is very different from what it would be if you are trying to intercept all links on the site. If your links are all in link fields, I implore you to read the first section. If that isn't the case with your solution, then - recognizing that the first section still had to be said - read onto the second section.

If your links are all coming from Link fields...

If your links are all coming from Link fields, then I think that you may be thinking too much about the HTML that you are planning to render and not enough about the data. It's something that I feel like every developer, myself included, goes through when learning a CMS. If you separate the data from the rendered markup in your mind, it doesn't matter whether you use an anchor tag to pop up your modal or a span.

If all of the links that you are referring to are link fields, then you can accomplish what you are looking for with a droplink that points to a footnote item that holds the data you plan to render in your attributes.

If your links are coming (or may come) from the RTE...

The solution for links that do not come from RTE fields is the same as in the first section. However, in order to intercept the way that Sitecore expands links inside of the RTE, you can create a custom LinkProvider that uses a custom LinkParser and custom ItemLinkExpander.

To do this, you will need to override the LinkProvider.CreateLinkParser method to return your custom LinkParser type, and you will need to make your LinkParser return your custom LinkExpander that will handle the "footnote links" inside of the RTE content.

To start, you will need to implement your new FootnoteSupportingLinkParser, like so:

public class FootnoteSupportingLinkParser : LinkProvider.LinkParser 
{
    public override IEnumerable<LinkExpander> GetExpanders() 
    {
        return (IEnumerable<LinkExpander>) new Collection<LinkExpander>()
        {
            (LinkExpander) new MediaLinkExpander(),
            (LinkExpander) new FootnoteSupportingItemLinkExpander()
        };
    }
}

Note that the type FootnoteSupportingItemLinkExpander does not actually exist. The idea, here, is to create your own implementation of the Sitecore.Links.ItemLinkExpander that will expand links to footnote items one way and links to other items the default way. You can do this like so:

public class FootnoteSupportingItemLinkExpander : ItemLinkExpander 
{
    public override void Expand(ref string text, UrlOptions urlOptions) 
    {
        Assert.ArgumentNotNull((object) text, "text");
        Assert.ArgumentNotNull((object) urlOptions, "urlOptions");
        int startIndex1 = text.IndexOf("~/link.aspx?", StringComparison.InvariantCulture);
        if (startIndex1 == -1)
            return;
        StringBuilder stringBuilder = new StringBuilder(text.Length);
        int startIndex2 = 0;
        for (; startIndex1 >= 0; startIndex1 = text.IndexOf("~/link.aspx?", startIndex2, StringComparison.InvariantCulture))
        {
            int num = text.IndexOf("_z=z", startIndex1, StringComparison.InvariantCulture);
            if (num < 0)
            {
                text = stringBuilder.ToString();
                return;
            }

            // TODO: change this to resolve target item (or null)
            string url = DynamicLink.Parse(text.Substring(startIndex1, num - startIndex1)).GetUrl(urlOptions);

            // TODO: write in logic to check if target item is a footnote item
            // TODO: if a footnote item, change the following to parse the footnote link as desired

            string str = text.Substring(startIndex2, startIndex1 - startIndex2);
            stringBuilder.Append(str);
            stringBuilder.Append(url);
            startIndex2 = num + "_z=z".Length;
        }
        stringBuilder.Append(text.Substring(startIndex2));
        text = stringBuilder.ToString();
    }
} 

Next, you will need to implement your new FootnoteSupportingLinkProvider, like so:

public class FootnoteSupportingLinkProvider : LinkProvider
{
    protected override LinkProvider.LinkParser CreateLinkParser() 
    {
        return new FootnoteSupportingLinkParser();
    }
}

Lastly, you will need to register your new FootnoteSupportingLinkProvider type, which you can do with the following patch:

<linkManager>
  <patch:attribute name="defaultProvider" value="footnoteSupportingProvider" />
  <providers>
    <add name="footnoteSupportingProvider" type="MyProject.MyNamespace.FootnoteSupportingLinkProvider, MyProject" />
  </providers>
</linkManager>

At this point, you should be ready for testing and your new LinkProvider should automatically render links created in the Rich Text Editor that point to your footnote items the way you want.

| improve this answer | |
  • In your answer for the second part you say "The solution for links that do not come from RTE fields is the same as in the first section." Does the expand function we are overriding not get called for link fields (is Expand strictly for rich text fields)? – Teeknow Jun 20 '17 at 13:28
  • Yes, that is correct. The ExpandDynamicLinks method is strictly for Rich Text fields. – Zachary Kniebel Jun 20 '17 at 13:41
  • Alright this would get me part of the way there then. I get what you're saying about separating the html from the data. I don't want to only support footnote links in some droplinks/some link fields though which is why I want to override it at a level similar to where it is being overridden for the rich text field. I believe what you're suggesting is something along the lines of creating a custom helper that will rendering links appropriately based on item type (footnote type vs any other). Please correct me if I misunderstood that. – Teeknow Jun 20 '17 at 14:15
  • I think you're on the right track. To clarify, the code in the 2nd section - once complete - will force all links to footnote items in Rich Text fields to render the way you choose (data-attributes, etc). As for the other links on the site stored in link field types, there is no way to standardize the way those render, but you can handle them manually, as described in the first section. – Zachary Kniebel Jun 20 '17 at 14:36
  • That is definitely true for types like droplinks. I'm going to combine your rich text answer with the linked answer (handles general link fields sitecore.stackexchange.com/questions/2955/…) in order to have as wide of a coverage as I can. I'll handle item and list field types on a case by case basis. Thanks for your help! – Teeknow Jun 20 '17 at 14:43
0

In order to keep this logic in one place I ended up doing the rtf logic in the render field pipeline too. This has a reference to two extension methods that will need to be substituted. They are InheritsFrom and ParseQueryString.

public abstract class ExpandLinkAttributes
{
    internal abstract Dictionary<ID, string> FieldIdToDataAttribute { get; }
    internal abstract ID TemplateId { get; }

    public void Process(global::Sitecore.Pipelines.RenderField.RenderFieldArgs args)
    {
        if (Context.PageMode.IsExperienceEditorEditing)
            return;

        var field = FieldTypeManager.GetField(args.GetField());

        if (args.FieldTypeKey == "rich text")
        {
            if (!string.IsNullOrEmpty(args.Result.FirstPart))
            {
                args.Result.FirstPart = SetRichTextAttributes(args.Result.FirstPart);
            }
            if (!string.IsNullOrEmpty(args.Result.LastPart))
            {
                args.Result.LastPart = SetRichTextAttributes(args.Result.LastPart);
            }
        }
        else if (field is LinkField)
        {
            LinkField linkField = (LinkField)field;

            SetLinkFieldAttribute(linkField, args);
        }
    }

    private void SetLinkFieldAttribute(LinkField linkField, global::Sitecore.Pipelines.RenderField.RenderFieldArgs args)
    {
        if (!linkField.IsInternal)
            return;

        var targetItem = linkField.TargetItem;

        if(targetItem?.InheritsFrom(TemplateId) != true)
            return;

        foreach (var fieldIdToDataAttribute in FieldIdToDataAttribute)
        {
            var fieldValue = HttpUtility.HtmlAttributeEncode(FieldRenderer.Render(targetItem, fieldIdToDataAttribute.Key.ToString()));
            args.Parameters.Add(fieldIdToDataAttribute.Value, fieldValue);
        }

        args.Parameters.Add("href", "javascript:void(0);");
    }

    protected string SetRichTextAttributes(string html)
    {
        var doc = new HtmlDocument();
        doc.LoadHtml(html);

        if (doc.DocumentNode == null)
            return html;

        var links = doc.DocumentNode.SelectNodes("//a[@href]");

        if (links == null)
            return html;

        foreach (var link in links)
        {
            var hrefAttribute = link.Attributes["href"];

            if (string.IsNullOrEmpty(hrefAttribute?.Value))
                continue;

            var urlParts = hrefAttribute.Value.Split('?');

            if (urlParts.Length != 2)
                continue;

            var queryString = urlParts[1].ParseQueryString();

            var shortId = queryString["_id"];

            Item item;
            if (!TryGetItem(shortId, out item) || !item.InheritsFrom(TemplateId))
                continue;

            foreach (var fieldIdToDataAttribute in FieldIdToDataAttribute)
            {
                var fieldValue = HttpUtility.HtmlAttributeEncode(FieldRenderer.Render(item, fieldIdToDataAttribute.Key.ToString()));
                link.Attributes.Add(fieldIdToDataAttribute.Value, fieldValue);
            }

            hrefAttribute.Value = "javascript:void(0);";
        }

        return doc.DocumentNode.OuterHtml;
    }

    private static bool TryGetItem(string itemIdText, out Item item)
    {
        if (!string.IsNullOrEmpty(itemIdText))
        {
            var itemId = ShortID.DecodeID(itemIdText);
            if (!itemId.IsNull && itemId != ID.Null)
            {
                if (Context.Database != null)
                {
                    item = Context.Database.GetItem(itemId);
                    return item != null;
                }
            }
        }

        item = default(Item);
        return false;
    }
}

Then I made a class that inherited from ExpandLinkAttributes and filled in the abstract properties. You can set this up to read from a config if necessary instead. Then add that custom class to a patch config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor patch:before="processor[@type='Sitecore.Pipelines.RenderField.ExpandLinks, Sitecore.Kernel']" 
                   type="My.Namespace.ClassInheritingExpandLinkAttributes, MyDll" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.