Home » Html Agility Pack

Category Archives: Html Agility Pack

Add JavaScript to the Client OnClick Event of the Sitecore WFFM Submit Button

A SDN forum thread popped up a week and a half ago asking whether it were possible to attach a Google Analytics event to the WFFM submit button — such would involve adding a snippet of JavaScript to the OnClick attribute of the WFFM submit button’s HTML — and I was immediately curious how one would go about achieving this, and whether this were possible at all.

I did a couple of hours of research last night — I experimented with custom processors of pipelines used by WFFM — but found no clean way of adding JavaScript to the OnClick event of the WFFM submit button.

However — right before I was about to throw in the towel for the night — I did find a solution on how one could achieve this — albeit not necessarily a clean solution since it involves some HTML manipulation (I would opine using the OnClientClick attribute of an ASP.NET Button to be cleaner, but couldn’t access the WFFM submit button due to its encapsulation and protection level in a WFFM WebControl) — via a custom Sitecore.Form.Core.Renderings.FormRender:

using System.IO;
using System.Linq;
using System.Web.UI;

using Sitecore.Form.Core.Renderings;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Form.Core.Renderings
{
    public class AddOnClientClickFormRender : FormRender
    {
        private const string ConfirmJavaScriptFormat = "if(!confirm('Are you sure you want to submit this form?')) {{ return false; }} {0} ";

        protected override void DoRender(HtmlTextWriter output)
        {
            string html = string.Empty;
            using (StringWriter stringWriter = new StringWriter())
            {
                using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter))
                {
                    base.DoRender(htmlTextWriter);
                }

                html = AddOnClientClickToSubmitButton(stringWriter.ToString());
            }

            output.Write(html);
        }

        private static string AddOnClientClickToSubmitButton(string html)
        {
            if (string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            HtmlNode submitButton = GetSubmitButton(html);
            if (submitButton == null && submitButton.Attributes["onclick"] != null)
            {
                return html;
            }

            submitButton.Attributes["onclick"].Value = string.Format(ConfirmJavaScriptFormat, submitButton.Attributes["onclick"].Value);
            return submitButton.OwnerDocument.DocumentNode.InnerHtml;
        }

        private static HtmlNode GetSubmitButton(string html)
        {
            HtmlNode documentNode = GetHtmlDocumentNode(html);
            return documentNode.SelectNodes("//input[@type='submit']").FirstOrDefault();
        }

        private static HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private static HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }
    }
}

The FormRender above uses Html Agility Pack — which comes with Sitecore — to retrieve the submit button in the HTML that is constructed by the base FormRender class, and adds a snippet of JavaScript to the beginning of the OnClick attribute (there is already JavaScript in this attribute, and we want to run our JavaScript first).

I didn’t wire up a Google Analytics event to the submit button in this FormRender — it would’ve required me to spin up an account for my local sandbox instance, and I feel this would’ve been overkill for this post.

Instead — as an example of adding JavaScript to the OnClick attribute of the WFFM submit button — I added code to launch a JavaScript confirmation dialog asking the form submitter whether he/she would like to continue submitting the form. If the user clicks the ‘Cancel’ button, the form is not submitted, and is submitted if the user clicks ‘OK’.

I then had to hook this custom FormRender to the WFFM Form Rendering — /sitecore/layout/Renderings/Modules/Web Forms for Marketers/Form — in Sitecore:

form-rendering

I then saved, published, and navigated to a WFFM test form. I then clicked the submit button:

confirmation-box

As you can see, I was prompted with a JavaScript confirmation dialog box.

If you have any thoughts on this implementation, or know of a better way to do this, please drop a comment.

Until next time, have a Sitecorelicious day! 🙂

Tailor Sitecore Item Web API Field Values On Read

Last week Sitecore MVP Kamruz Jaman asked me in this tweet if I could answer this question on Stack Overflow.

The person asking the question wanted to know why alt text for images aren’t returned in responses from the Sitecore Item Web API, and was curious if it were possible to include these.

After digging around the Sitecore.ItemWebApi.dll and my local copy of /App_Config/Include/Sitecore.ItemWebApi.config — this config file defines a bunch of pipelines and their processors that can be augmented or overridden — I learned field values are returned via logic in the Sitecore.ItemWebApi.Pipelines.Read.GetResult class, which is exposed in /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] in /App_Config/Include/Sitecore.ItemWebApi.config:

sitecore-item-webapi-raw-field-value

This is an example of a raw value for an image field — it does not include the alt text for the image:

image-xml-content-editor

I spun up a copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to show the value returned by the above pipeline processor for an image field:

image-xml-out-of-the-box

The Sitecore.ItemWebApi.Pipelines.Read.GetResult class exposes a virtual method hook — the protected method GetFieldInfo() — that allows custom code to change a field’s value before it is returned.

I wrote the following class as an example for changing an image field’s value:

using System;
using System.IO;
using System.Web;
using System.Web.UI;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
    public class EnsureImageFieldAltText : GetResult
    {
        protected override Dynamic GetFieldInfo(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            Dynamic dynamic = base.GetFieldInfo(field);
            AddAltTextForImageField(dynamic, field);
            return dynamic;
        }

        private static void AddAltTextForImageField(Dynamic dynamic, Field field)
        {
            Assert.ArgumentNotNull(dynamic, "dynamic");
            Assert.ArgumentNotNull(field, "field");

            if(IsImageField(field))
            {
                dynamic["Value"] = AddAltTextToImages(field.Value, GetAltText(field));
            }
        }

        private static string AddAltTextToImages(string imagesXml, string altText)
        {
            if (string.IsNullOrWhiteSpace(imagesXml) || string.IsNullOrWhiteSpace(altText))
            {
                return imagesXml;
            }
            
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(imagesXml);
            HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//image");
            foreach (HtmlNode image in images)
            {
                if (image.Attributes["src"] != null)
                {
                    image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
                }
                
                image.SetAttributeValue("alt", altText);
            }
            
            return htmlDocument.DocumentNode.InnerHtml;
        }

        private static string GetAbsoluteUrl(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            Uri uri = HttpContext.Current.Request.Url;

            if (url.StartsWith(uri.Scheme))
            {
                return url;
            }

            string port = string.Empty;

            if (uri.Port != 80)
            {
                port = string.Concat(":", uri.Port);
            }

            return string.Format("{0}://{1}{2}/~{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
        }

        private static string GetAltText(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            if (IsImageField(field))
            {
                ImageField imageField = field;
                if (imageField != null)
                {
                    return imageField.Alt;
                }
            }

            return string.Empty;
        }

        private static bool IsImageField(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            return field.Type == "Image";
        }
    }
}

The class above — with the help of the Sitecore.Data.Fields.ImageField class — gets the alt text for the image, and adds a new alt XML attribute to the XML before it is returned.

The class also changes the relative url defined in the src attribute in to be an absolute url.

I then swapped out /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] with the class above in /App_Config/Include/Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- Lots of stuff here -->
      <!-- Handles the item read operation. -->
		<itemWebApiRead>
			<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.EnsureImageFieldAltText, Sitecore.Sandbox" />
		</itemWebApiRead>
		  <!--Lots of stuff here too -->
		
    </pipelines>
	<!-- Even more stuff here -->
  </sitecore>
</configuration>

I then reran the console application to see what the XML now looks like, and as you can see the new alt attribute was added:

alt-image-xml

You might be thinking “Mike, image field XML values are great in Sitecore’s Content Editor, but client code consuming this data might have trouble with it. Is there anyway to have HTML be returned instead of XML?

You bet!

The following subclass of Sitecore.ItemWebApi.Pipelines.Read.GetResult returns HTML, not XML:

using System;
using System.IO;
using System.Web;
using System.Web.UI;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
    public class TailorFieldValue : GetResult
    {
        protected override Dynamic GetFieldInfo(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            Dynamic dynamic = base.GetFieldInfo(field);
            TailorValueForImageField(dynamic, field);
            return dynamic;
        }

        private static void TailorValueForImageField(Dynamic dynamic, Field field)
        {
            Assert.ArgumentNotNull(dynamic, "dynamic");
            Assert.ArgumentNotNull(field, "field");

            if (field.Type == "Image")
            {
                dynamic["Value"] = SetAbsoluteUrlsOnImages(GetImageHtml(field));
            }
        }

        private static string SetAbsoluteUrlsOnImages(string html)
        {
            if (string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//img");
            foreach (HtmlNode image in images)
            {
                if (image.Attributes["src"] != null)
                {
                    image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
                }
            }

            return htmlDocument.DocumentNode.InnerHtml;
        }

        private static string GetAbsoluteUrl(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            Uri uri = HttpContext.Current.Request.Url;

            if (url.StartsWith(uri.Scheme))
            {
                return url;
            }

            string port = string.Empty;

            if (uri.Port != 80)
            {
                port = string.Concat(":", uri.Port);
            }

            return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
        }

        private static string GetImageHtml(Field field)
        {
            return GetImageHtml(field.Item, field.Name);
        }

        private static string GetImageHtml(Item item, string fieldName)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(fieldName, "fieldName");
            return RenderImageControlHtml(new Image { Item = item, Field = fieldName });
        }

        private static string RenderImageControlHtml(Image image)
        {
            Assert.ArgumentNotNull(image, "image");
            string html = string.Empty;

            using (TextWriter textWriter = new StringWriter())
            {
                using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(textWriter))
                {
                    image.RenderControl(htmlTextWriter);
                }

                html = textWriter.ToString();
            }

            return html;
        }
    }
}

The class above uses an instance of the Image field control (Sitecore.Web.UI.WebControls.Image) to do all the work for us around building the HTML for the image, and we also make sure the url within it is absolute — just as we had done above.

I then wired this up to my local Sitecore instance in /App_Config/Include/Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- Lots of stuff here -->
      <!-- Handles the item read operation. -->
		<itemWebApiRead>
			<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.TailorFieldValue, Sitecore.Sandbox" />
		</itemWebApiRead>
		  <!--Lots of stuff here too -->
		
    </pipelines>
	<!-- Even more stuff here -->
  </sitecore>
</configuration>

I then executed the console application, and was given back HTML for the image:

image-html-returned

If you can think of other reasons for manipulating field values in subclasses of Sitecore.ItemWebApi.Pipelines.Read.GetResult, please drop a comment.

Addendum
Kieran Marron — a Lead Developer at Sitecore — wrote another Sitecore.ItemWebApi.Pipelines.Read.GetResult subclass example that returns an image’s alt text in the Sitecore Item Web API response via a new JSON property. Check it out!

Dude, Where’s My Processor? Filling the Void in the SaveRichTextContent and LoadRichTextContent Sitecore Pipelines

Some of you might be aware that I frequently go through the Web.config of my local instance of Sitecore looking for opportunities to extend or customize class files referenced within it — I may have mentioned this in a previous post, and no doubt have told some Sitecore developers/enthusiasts in person I do this at least once per day. I must confess: I usually do this multiple times a day.

Last night, I was driven to explore something I have noticed in the Web.config of my v6.5 instance — my attention has been usurped many times by the saveRichTextContent and loadRichTextContent pipeline nodes being empty.

These two pipelines allow you to make changes to content within Rich Text fields before any save actions on your item in the Sitecore client.

I remembered that one of them did have a pipeline processor defined within it at one point. It was time to do some research.

After conducting some research — truth be told, I only googled a couple of times — I stumbled upon some release notes on SDN discussing the saveRichTextContent Web.config pipeline, and that this pipeline did contain a processor in it at one point — the Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.EmbedInParagraph processor — although I don’t remember what this processor did, and don’t have an older version of Sitecore.Client.dll to investigate. I could download an older version of Sitecore from SDN, but decided to leave that exercise for another snowy weekend.

I decided to explore whether the option to add custom processors to these pipelines still existed. I came up with an idea straight out of the 1990’s — having marquee tags animate content across my pages.

As an aside, back in the 1990’s, almost every webpage — all webpages were called homepages then — had at least one marquee. Most had multiple — it was the cool thing to do back then, asymptotic only to having an ‘Under Construction’ image on your homepage. Employing this practice today would be considered anathema.

I decided to reuse my concept of manipulator from my Manipulate Field Values in a Custom Sitecore Web Forms for Marketers DataProvider article, and created a new manipulator to wrap specified tags in marquee tags:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IWrapHtmlTagsInTagManipulator : IManipulator<string>
    {
    }
}

I thought it would be a good idea to define a DTO for my manipulator to pass objects to it in a clean manner:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WrapHtmlTagsInTagManipulatorSettings
    {
        public string WrapperTag { get; set; }
        public IEnumerable<string> TagsToWrap { get; set; }
    }
}

Next, I built my manipulator:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class WrapHtmlTagsInTagManipulator : IWrapHtmlTagsInTagManipulator
    {
        private WrapHtmlTagsInTagManipulatorSettings Settings { get; set; }

        private WrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            SetSettings(settings);
        }

        private void SetSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            AssertSettings(settings);
            Settings = settings;
        }

        private static void AssertSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            Assert.ArgumentNotNull(settings, "settings");
            Assert.ArgumentNotNullOrEmpty(settings.WrapperTag, "settings.WrapperTag");
            Assert.ArgumentNotNull(settings.TagsToWrap, "settings.TagsToWrap");
        }

        public string Manipulate(string html)
        {
            Assert.ArgumentNotNullOrEmpty(html, "html");
            HtmlNode documentNode = GetHtmlDocumentNode(html);

            foreach (string tagToWrap in Settings.TagsToWrap)
            {
                WrapTags(documentNode, tagToWrap);
            }

            return documentNode.InnerHtml;
        }

        private void WrapTags(HtmlNode documentNode, string tagToWrap)
        {
            HtmlNodeCollection htmlNodes = documentNode.SelectNodes(CreateNewDescendantsSelector(tagToWrap));

            foreach(HtmlNode htmlNode in htmlNodes)
            {
                WrapHtmlNodeIfApplicable(documentNode, htmlNode);
            }
        }

        private void WrapHtmlNodeIfApplicable(HtmlNode documentNode, HtmlNode htmlNode)
        {
            if (!AreEqualIgnoreCase(htmlNode.ParentNode.Name, Settings.WrapperTag))
            {
                WrapHtmlNode(documentNode, htmlNode, Settings.WrapperTag);
            }
        }

        private static void WrapHtmlNode(HtmlNode documentNode, HtmlNode htmlNode, string wrapperTag)
        {
            HtmlNode wrapperHtmlNode = documentNode.OwnerDocument.CreateElement(wrapperTag);
            AddNewParent(wrapperHtmlNode, htmlNode);
        }

        private static void AddNewParent(HtmlNode newParentHtmlNode, HtmlNode htmlNode)
        {
            Assert.ArgumentNotNull(newParentHtmlNode, "newParentHtmlNode");
            Assert.ArgumentNotNull(htmlNode, "htmlNode");
            htmlNode.ParentNode.ReplaceChild(newParentHtmlNode, htmlNode);
            newParentHtmlNode.AppendChild(htmlNode);
        }

        private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
        {
            return string.Equals(stringOne, stringTwo, StringComparison.InvariantCultureIgnoreCase);
        }

        private static string CreateNewDescendantsSelector(string tag)
        {
            Assert.ArgumentNotNullOrEmpty(tag, "tag");
            return string.Format("//{0}", tag);
        }

        private HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }

        public static IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            return new WrapHtmlTagsInTagManipulator(settings);
        }
    }
}

My manipulator class above uses Html Agility Pack to find targeted html elements, and wrap them in newly created marquee tags — which are also created via Html Agility Pack.

I decided to create a base class to contain core logic that will be used across both of my pipeline processors:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Manipulators.DTO;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base
{
    public abstract class AddSomeMarqueesBase
    {
        private IWrapHtmlTagsInTagManipulator _HtmlManipulator;
        private IWrapHtmlTagsInTagManipulator HtmlManipulator
        {
            get
            {
                if(_HtmlManipulator == null)
                {
                    _HtmlManipulator = CreateNewWrapHtmlTagsInTagManipulator();
                }

                return _HtmlManipulator;
            }
        }

        private IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator()
        {
            return WrapHtmlTagsInTagManipulator.CreateNewWrapHtmlTagsInTagManipulator(CreateNewWrapHtmlTagsInTagManipulatorSettings());
        }

        protected virtual WrapHtmlTagsInTagManipulatorSettings CreateNewWrapHtmlTagsInTagManipulatorSettings()
        {
            return new WrapHtmlTagsInTagManipulatorSettings
            {
                WrapperTag = "marquee",
                TagsToWrap = new string[] { "em", "img" }
            };
        }

        protected virtual string ManipulateHtml(string html)
        {
            if (!string.IsNullOrEmpty(html))
            {
                return HtmlManipulator.Manipulate(html);
            }

            return html;
        }
    }
}

This base class creates an instance of our manipulator class above, passing in the required DTO housing the wrapper tag and tags to wrap settings.

Honestly, while writing this article and looking at this code, I am not completely happy about how I implemented this base class. I should have added a constructor which takes in the manipulator instance — thus allowing subclasses to provide their own manipulators, especially if these subclasses need to use a different manipulator than the one used by default in the base class.

Further, it probably would have been prudent to put the html tags I defined in my DTO instance into a patch config file.

Next, I defined my loadRichTextContent pipeline processor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(LoadRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Followed by my saveRichTextContentpipeline processor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(SaveRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Thereafter, I glued everything together via a patch config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <loadRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </loadRichTextContent>
      <saveRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </saveRichTextContent>
    </pipelines>
  </sitecore>
</configuration>

Time to see the fruits of my labor above.

I’ve added some content in a Rich Text field:

RTF-Design-Before-Marquees

Here’s the html in the Rich Text field:

RTF-Html-Before-Marquees

I clicked the ‘Accept’ button in the Rich Text dialog window, and then saw the targeted content come to life:

RTF-With-Marquees

I launched the dialog window again to investigate what the html now looks like:

RTF-Html-With-Marquees

Mission accomplished — we now have marquees! 🙂

I do want to point out I could not get my loadRichTextContent pipeline processor to run. I thought it would run when opening the Rich Text dialog, although I was wrong — it did not. I also tried to get it to run via the ‘Edit Html’ button, but to no avail.

If I am looking in the wrong place, or this is a known issue in Sitecore, please drop a comment and let me know.

Custom Sitecore Rich Text Editor Button: Inserting Dynamic Content

Last Thursday, I stumbled upon an article discussing how to create and add a custom button to the Rich Text Editor in Sitecore. This article referenced an article written by Mark Stiles — his article set the foundation for me to do this very thing last Spring for a client.

Unlike the two articles above, I had to create two different buttons to insert dynamic content — special html containing references to other items in the Sitecore content tree via Item IDs — which I would ‘fix’ via a RenderField pipeline when a user would visit the page containing this special html. I had modeled my code around the ‘Insert Link’ button by delegating to a helper class to ‘expand’ my dynamic content as the LinkManager class does for Sitecore links.

The unfortunate thing is I cannot show you that code – it’s proprietary code owned by a previous employer.

Instead, I decided to build something similar to illustrate how I did this. I will insert special html that will ultimately transform into jQuery UI dialogs.

First, I needed to create items that represent dialog boxes. I created a new template with two fields — one field containing the dialog box’s heading and the other field containing copy that will go inside of the dialog box:

Dialog Content Item Template

I then created three dialog box items with content that we will use later on when testing:

Dialog Content Items

With the help of xml defined in /sitecore/shell/Controls/Rich Text Editor/InsertLink/InsertLink.xml and /sitecore/shell/Controls/Rich Text Editor/InsertImage/InsertImage.xml, I created my new xml control definition in a file named /sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.xml:

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
	<RichText.InsertDialog>
	
		<!-- Don't forget to set your icon 🙂 -->
		<FormDialog Icon="Business/32x32/message.png" Header="Insert a Dialog" Text="Select the dialog content item you want to insert." OKButton="Insert Dialog">
			
			<!-- js reference to my InsertDialog.js script. -->
			<!-- For some strange reason, if the period within the script tag is not present, the dialog form won't work -->
			<script Type="text/javascript" src="/sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.js">.</script>
			
			<!-- Reference to my InsertDialogForm class -->
			<CodeBeside Type="Sitecore.Sandbox.RichTextEditor.InsertDialog.InsertDialogForm,Sitecore.Sandbox" />

			<!-- Root contains the ID of /sitecore/content/Dialog Content Items -->
			<DataContext ID="DialogFolderDataContext"  Root="{99B14D44-5A0F-43B6-988E-94197D73B348}" />
			<GridPanel Width="100%" Height="100%" Style="table-layout:fixed">			
				<GridPanel Width="100%" Height="100%" Style="table-layout:fixed" Columns="3" GridPanel.Height="100%">
					<Scrollbox Class="scScrollbox scFixSize" Width="100%" Height="100%" Background="white" Border="1px inset" Padding="0" GridPanel.Height="100%" GridPanel.Width="50%" GridPanel.Valign="top">
						<TreeviewEx ID="DialogContentItems" DataContext="DialogFolderDataContext" Root="true" />
					</Scrollbox>
				</GridPanel>
			</GridPanel>
		</FormDialog>
	</RichText.InsertDialog>
</control>

My dialog form will house one lonely TreeviewEx containing all dialog items in the /sitecore/content/Dialog Content Items folder I created above.

I then created the ‘CodeBeside’ DialogForm class to accompany the xml above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework;
using Sitecore.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Pages;
using Sitecore.Web.UI.Sheer;
using Sitecore.Web.UI.WebControls;

namespace Sitecore.Sandbox.RichTextEditor.InsertDialog
{
    public class InsertDialogForm : DialogForm
    {
        protected DataContext DialogFolderDataContext;
        protected TreeviewEx DialogContentItems;

        protected string Mode
        {
            get
            {
                string mode = StringUtil.GetString(base.ServerProperties["Mode"]);

                if (!string.IsNullOrEmpty(mode))
                {
                    return mode;
                }

                return "shell";
            }
            set
            {
                Assert.ArgumentNotNull(value, "value");
                base.ServerProperties["Mode"] = value;
            }
        }
        
        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);

            if (!Context.ClientPage.IsEvent)
            {
                Inialize();
            }
        }

        private void Inialize()
        {
            SetMode();
            SetDialogFolderDataContextFromQueryString();
        }

        private void SetMode()
        {
            Mode = WebUtil.GetQueryString("mo");
        }

        private void SetDialogFolderDataContextFromQueryString()
        {
            DialogFolderDataContext.GetFromQueryString();
        }
        
        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");

            string selectedItemID = GetSelectedItemIDAsString();

            if (string.IsNullOrEmpty(selectedItemID))
            {
                return;
            }

            string selectedItemPath = GetSelectedItemPath();
            string javascriptArguments = string.Format("{0}, {1}", EscapeJavascriptString(selectedItemID), EscapeJavascriptString(selectedItemPath));

            if (IsWebEditMode())
            {
                SheerResponse.SetDialogValue(javascriptArguments);
                base.OnOK(sender, args);
            }
            else
            {
                string closeJavascript = string.Format("scClose({0})", javascriptArguments);
                SheerResponse.Eval(closeJavascript);
            }
        }

        private string GetSelectedItemIDAsString()
        {
            ID selectedID = GetSelectedItemID();

            if (selectedID != ID.Null)
            {
                return selectedID.ToString();
            }

            return string.Empty;
        }

        private ID GetSelectedItemID()
        {
            Item selectedItem = GetSelectedItem();

            if (selectedItem != null)
            {
                return selectedItem.ID;
            }

            return ID.Null;
        }

        private string GetSelectedItemPath()
        {
            Item selectedItem = GetSelectedItem();

            if (selectedItem != null)
            {
                return selectedItem.Paths.FullPath;
            }

            return string.Empty;
        }

        private Item GetSelectedItem()
        {
            return DialogContentItems.GetSelectionItem();
        }

        private static string EscapeJavascriptString(string stringToEscape)
        {
            return StringUtil.EscapeJavascriptString(stringToEscape);
        }
        
        protected override void OnCancel(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");

            if (IsWebEditMode())
            {
                base.OnCancel(sender, args);
            }
            else
            {
                SheerResponse.Eval("scCancel()");
            }
        }

        private bool IsWebEditMode()
        {
            return string.Equals(Mode, "webedit", StringComparison.InvariantCultureIgnoreCase);
        }
    }
} 

This DialogForm basically sends a selected Item’s ID and path back to the client via the scClose() function defined below in a new file named /sitecore/shell/Controls/Rich Text Editor/InsertDialog/InsertDialog.js:

function GetDialogArguments() {
    return getRadWindow().ClientParameters;
}

function getRadWindow() {
  if (window.radWindow) {
        return window.radWindow;
  }
    
    if (window.frameElement && window.frameElement.radWindow) {
        return window.frameElement.radWindow;
    }
    
    return null;
}

var isRadWindow = true;

var radWindow = getRadWindow();

if (radWindow) { 
  if (window.dialogArguments) { 
    radWindow.Window = window;
  } 
}

function scClose(dialogContentItemId, dialogContentItemPath) {
	// we're passing back an object holding data needed for inserting our special html into the RTE
	var dialogInfo = {
		dialogContentItemId: dialogContentItemId,
		dialogContentItemPath: dialogContentItemPath
	};

	getRadWindow().close(dialogInfo);
}

function scCancel() {
  getRadWindow().close();
}

if (window.focus && Prototype.Browser.Gecko) {
  window.focus();
}

I then had to add a new javascript command in /sitecore/shell/Controls/Rich Text Editor/RichText Commands.js to open my dialog form and map this to a callback function — which I named scInsertDialog to follow the naming convention of other callbacks within this script file — to handle the dialogInfo javascript object above that is passed to it:

RadEditorCommandList["InsertDialog"] = function(commandName, editor, args) {
	scEditor = editor;
	
	editor.showExternalDialog(
		"/sitecore/shell/default.aspx?xmlcontrol=RichText.InsertDialog&la=" + scLanguage,
		null, //argument
		500,
		400,
		scInsertDialog, //callback
		null, // callback args
		"Insert Dialog",
		true, //modal
		Telerik.Web.UI.WindowBehaviors.Close, // behaviors
		false, //showStatusBar
		false //showTitleBar
	);
};

function scInsertDialog(sender, dialogInfo) {
	if (!dialogInfo) {
		return;
	}
	
	// build our special html to insert into the RTE
	var placeholderHtml = "<hr class=\"dialog-placeholder\" style=\"width: 100px; display: inline-block; height: 20px;border: blue 4px solid;\"" 
						+ "data-dialogContentItemId=\"" + dialogInfo.dialogContentItemId +"\" "
						+ "title=\"Dialog Content Path: " + dialogInfo.dialogContentItemPath + "\" />";
					  
	scEditor.pasteHtml(placeholderHtml, "DocumentManager");
}

My callback function builds the special html that will be inserted into the Rich Text Editor. I decided to use a <hr /> tag to keep my html code self-closing — it’s much easier to use a self-closing html tag when doing this, since you don’t have to check whether you’re inserting more special html into preexisting special html. I also did this for the sake of brevity.

I am using the title attribute in my special html in order to assist content authors in knowing which dialog items they’ve inserted into rich text fields. When a dialog blue box is hovered over, a tooltip containing the dialog item’s content path will display.

I’m not completely satisfied with building my special html in this javascript file. It probably should be moved into C# somewhere to prevent the ease of changing it — someone could easily just change it without code compilation, and break how it’s rendered to users. We need this html to be a certain way when ‘fixing’ it in our RenderField pipeline defined later in this article.

I then went into the core database and created a new Rich Text Editor profile under /sitecore/system/Settings/Html Editor Profiles:

Duplicated Rich Text Profile

Next, I created a new Html Editor Button (template: /sitecore/templates/System/Html Editor Profiles/Html Editor Button) using the __Html Editor Button master — wow, I never thought I would use the word master again when talking about Sitecore stuff 🙂 — and set its click and icon fields:

Insert Dialog Html Editor Button 1

Insert Dialog Html Editor Button 2

Now, we need to ‘fix’ our special html by converting it into presentable html to the user. I do this using the following RenderField pipeline:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.RenderField;
using Sitecore.Xml.Xsl;

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class ExpandDialogContent
    {
        public void Process(RenderFieldArgs renderFieldArgs)
        {
            if (ShouldFieldBeProcessed(renderFieldArgs))
            {
                ExpandDialogContentTags(renderFieldArgs);
            }
        }

        private bool ShouldFieldBeProcessed(RenderFieldArgs renderFieldArgs)
        {
            return renderFieldArgs.FieldTypeKey.ToLower() == "rich text";
        }

        private void ExpandDialogContentTags(RenderFieldArgs renderFieldArgs)
        {
            HtmlNode documentNode = GetHtmlDocumentNode(renderFieldArgs.Result.FirstPart);
            HtmlNodeCollection dialogs = documentNode.SelectNodes("//hr[@class='dialog-placeholder']");

            foreach (HtmlNode dialogPlaceholder in dialogs)
            {
                HtmlNode dialog = CreateDialogHtmlNode(dialogPlaceholder);

                if (dialog != null)
                {
                    dialogPlaceholder.ParentNode.ReplaceChild(dialog, dialogPlaceholder);
                }
                else
                {
                    dialogPlaceholder.ParentNode.RemoveChild(dialogPlaceholder);
                }
            }

            renderFieldArgs.Result.FirstPart = documentNode.InnerHtml;
        }

        private HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }

        private HtmlNode CreateDialogHtmlNode(HtmlNode dialogPlaceholder)
        {
            string dialogContentItemId = dialogPlaceholder.Attributes["data-dialogContentItemId"].Value;
            Item dialogContentItem = TryGetItem(dialogContentItemId);

            if(dialogContentItem != null)
            {
                string heading = dialogContentItem["Dialog Heading"];
                string content = dialogContentItem["Dialog Content"];

                return CreateDialogHtmlNode(dialogPlaceholder.OwnerDocument, heading, content);
            }

            return null;
        }

        private Item TryGetItem(string id)
        {
            try
            {
                return Context.Database.Items[id];
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return null;
        }

        private static HtmlNode CreateDialogHtmlNode(HtmlDocument htmlDocument, string heading, string content)
        {
            if (string.IsNullOrEmpty(content))
            {
                return null;
            }

            HtmlNode dialog = htmlDocument.CreateElement("div");
            dialog.Attributes.Add("class", "dialog");
            dialog.Attributes.Add("title", heading);
            dialog.InnerHtml = content;

            return dialog;
        }
    }
}

The above uses Html Agility Pack for finding all instances of my special <hr /> tags and creates new html that my jQuery UI dialog code expects. If you’re not using Html Agility Pack, I strongly recommend checking it out — it has saved my hide on numerous occasions.

I then inject this RenderField pipeline betwixt others via a new patch include file named /App_Config/Include/ExpandDialogContent.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
			<renderField>
				<processor type="Sitecore.Sandbox.Pipelines.RenderField.ExpandDialogContent, Sitecore.Sandbox" 
					patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" />
			</renderField>	
		</pipelines>
	</sitecore>
</configuration>

Now, it’s time to see if all of my hard work above has paid off.

I set the rich text field on my Sample Item template to use my new Rich Text Editor profile:

RTF source

Using the template above, I created a new test item, and opened its rich text field’s editor:

Opened RTE

I then clicked my new ‘Insert Dialog’ button and saw Dialog items I could choose to insert:

Insert Dialog Dialog Form RTF

Since I’m extremely excited, I decided to insert them all:

Inserted Three Dialogs WYSIWYG

I then forgot which dialog was which — the three uniform blue boxes are throwing me off a bit — so I hovered over the first blue box and saw that it was the first dialog item:

Inserted Three Dialogs WYSIWYG Hover

I then snuck a look at the html inserted — it was formatted the way I expected:

Inserted Three Dialogs HTML

I then saved my item, published and navigated to my test page. My RenderField pipeline fixed the special html as I expected:

Rich Text Button Test

I would like to point out that I had to add link and script tags to reference jQuery UI’s css and js files — including the jQuery library — and initialized my dialogs using the jQuery UI Dialog constructor. I have omitted this code.

This does seem like a lot of code to get something so simple to work.

However, it is worth the effort. Not only will you impress your boss and make your clients happy, you’ll also be dubbed the ‘cool kid’ at parties. You really will, I swear. 🙂

Until next time, have a Sitecoretastic day!