Home » Rich Text

Category Archives: Rich Text

Embedded Tweets in Sitecore: A Proof of Concept

In a previous post, I showcased a “proof of concept” for shortcodes in Sitecore — this is a shorthand notation for embedding things like YouTube videos in your webpages without having to type up a bunch of HTML — and felt I should follow up with another “proof of concept” around incorporating Embedded Tweets in Sitecore.

You might be asking “what’s an Embedded Tweet?” An Embedded Tweet is basically the process of pasting a Tweet URL from Twitter into an editable content area of your website/blog/whatever (think Rich Text field in Sitecore), and let the code that builds the HTML for your site figure out how to display it.

For example, I had used an Embedded Tweet in a recent post:

tweet-url-wordpress

This is what is seen on the rendered page:

tweet-embedded

While doing some research via Google on how to do this in Sitecore, I found this page from Twitter that discusses how you could go about accomplishing this, and discovered how to get JSON containing information about a Tweet — including its HTML — using one of Twitter’s API URLs:

tweet-api-json

The JSON above drove me to build the following POCO class to represent data returned by that URL:

using System.Runtime.Serialization;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class Tweet
    {
        [DataMember(Name = "cache_age")]
        public int CacheAgeMilliseconds { get; set; }

        [DataMember(Name = "url")]
        public string Url { get; set; }

        [DataMember(Name = "html")]
        public string Html { get; set; }
    }
}

I decided to omit some of the JSON properties returned by the Twitter URL from my class above — width and height are examples — since I felt I did not need to use them for this “proof of concept”.

I then leveraged the class above in the following class that will serve as a <renderField> pipeline processor to embed Tweets:

using System;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

using Sitecore.Caching;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class ExpandTweets
    {
        private string TwitterWidgetScriptTag {get ; set; }

        private string TwitterApiUrlFormat { get; set; }

        private string _TweetPattern;
        private string TweetPattern 
        {
            get
            {
                return _TweetPattern;
            }
            set
            {
                _TweetPattern = value;
                if (!string.IsNullOrWhiteSpace(_TweetPattern))
                {
                    _TweetPattern = HttpUtility.HtmlDecode(_TweetPattern);
                }
            }
        }

        private HtmlCache _HtmlCache;
        private HtmlCache HtmlCache
        {
            get
            {
                if (_HtmlCache == null)
                {
                    _HtmlCache = CacheManager.GetHtmlCache(Context.Site);
                }

                return _HtmlCache;
            }
        }

        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            AssertRequired();
            if(!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = ExpandTweetUrls(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private void AssertRequired()
        {
            Assert.IsNotNullOrEmpty(TwitterWidgetScriptTag, "TwitterWidgetScriptTag must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TwitterApiUrlFormat, "TwitterApiUrlFormat must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TweetPattern, "TweetPattern must be set! Check your configuration!");
        }

        protected virtual string ExpandTweetUrls(string html)
        {
            string htmlExpanded = html;
            MatchCollection matches = Regex.Matches(htmlExpanded, TweetPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
            foreach (Match match in matches)
            {
                string tweetHtml = GetTweetHtml(match.Groups["id"].Value);
                if (!string.IsNullOrWhiteSpace(tweetHtml))
                {
                    htmlExpanded = htmlExpanded.Replace(match.Value, tweetHtml);
                }
            }

            if (matches.Count > 0)
            {
                htmlExpanded = string.Concat(htmlExpanded, TwitterWidgetScriptTag);
            }

            return htmlExpanded;
        }

        protected virtual string GetTweetHtml(string id)
        {
            string html = GetTweetHtmlFromCache(id);
            if (!string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            Tweet tweet = GetTweetFromApi(id);
            AddTweetHtmlToCache(id, tweet);
            return tweet.Html;
        }

        private string GetTweetHtmlFromCache(string id)
        {
            return HtmlCache.GetHtml(id);
        }

        private void AddTweetHtmlToCache(string id, Tweet tweet)
        {
            if (string.IsNullOrWhiteSpace(tweet.Html))
            {
                return;
            }

            if (tweet.CacheAgeMilliseconds > 0)
            {
                HtmlCache.SetHtml(id, tweet.Html, DateTime.Now.AddMilliseconds(tweet.CacheAgeMilliseconds));
                return;
            }

            HtmlCache.SetHtml(id, tweet.Html);
        }

        protected virtual Tweet GetTweetFromApi(string id)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(TwitterApiUrlFormat, id));
            try
            {
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var result = reader.ReadToEnd();
                    JObject jObject = JObject.Parse(result);
                    return JsonConvert.DeserializeObject<Tweet>(jObject.ToString());
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return new Tweet { Html = string.Empty };
        }
    }
}

Methods in the class above find all Tweet URLs in the Rich Text, Single-Line Text, or Multi-Line Text field being processed — the code determines if it’s a Tweet URL based on a pattern that is supplied by a configuration setting (you will see this below in this post); extract Tweets’ Twitter identifiers (these are located at the end of the Tweet URLs); and attempt to find the Tweets’ HTML in Sitecore’s HTML cache.

If the HTML is found in cache for a Tweet, we return it. Otherwise, we make a request to Twitter’s API to get it, put it in cache one we have it (it is set to expire after a specified number of milliseconds from the time it was retrieved: Twitter returns the number of milliseconds in one full year by default), and then we return it.

If the returned HTML is not empty, we replace it in the field’s value for display.

If the HTML returned is empty — this could happen when an exception is encountered during the Twitter API call (of course we log the exception in the Sitecore log when this happens 😉 ) — we don’t touch the Tweet URL in the field’s value.

Once all Tweet URLs have been processed, we append a script tag referencing Twitter’s widget.js file — this is supplied through a configuration setting, and it does the heavy lifting on making the Tweet HTML look Twitterific 😉 — to the field’s rendered HTML.

I then tied everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.Tweets.ExpandTweets, Sitecore.Sandbox"
					patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']">
          <TwitterWidgetScriptTag>&lt;script async src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</TwitterWidgetScriptTag>
          <TwitterApiUrlFormat>https://api.twitter.com/1/statuses/oembed.json?id={0}&amp;omit_script=true</TwitterApiUrlFormat>
          <TweetPattern>https://twitter.com/.+/status/(?&lt;id&gt;\d*)</TweetPattern>
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see this in action!

I created a test Item, and added some legitimate and bogus Tweet URLs into one of its Rich Text fields (please pardon the grammatical issues in the following screenshots :-/):

tweets-rich-text

This isn’t the most aesthetically pleasing HTML, but it will serve its purpose for testing:

tweets-rich-text-html

After saving and publishing, I navigated to my test Item’s page, and saw this:

tweets-front-end

If you have any suggestions on making this better, or have other ideas for embedding Tweets in Sitecore, please share in a comment.

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!