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:
I then created three dialog box items with content that we will use later on when testing:
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:
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:
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:
Using the template above, I created a new test item, and opened its rich text field’s editor:
I then clicked my new ‘Insert Dialog’ button and saw Dialog items I could choose to insert:
Since I’m extremely excited, I decided to insert them all:
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:
I then snuck a look at the html inserted — it was formatted the way I expected:
I then saved my item, published and navigated to my test page. My RenderField pipeline fixed the special html as I expected:
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!