Extend Sitecore rich text hyperlink dialog

This is part 2 of 2 in a series of how to extend the link functionality in Sitecore to accomodate something custom. In our case, we are querying an external api for search results to use as choices for our link. For more context, check out part 1 here. Here is what we are making.

A hyperlink is added to the “Hyperlink” tab of the Hyperlink manager dialog box. When that link is clicked, I am showing an overlay on top of the hyperlink div. After the author enters a search term and clicks the “Search” button, the table of results appears. If the author double clicks on url, the url is added to the “Url” text box on the “Hyperlink” tab and the overlay closes. If the “X” is clicked, the overlay closes.

Extract the LinkManager.ascx file

The first thing we need to do is make a copy of the LinkManager that is embedded in the Telerik.Web.UI assembly. Find the LinkManager.ascx in the resources and right click to save the resource as a file.

I am adding this file to this path in the solution. SampleSite\sitecore\shell\Controls\Rich Text Editor\Dialogs\LinkManager.ascx. if you look at that path in the wwwroot of your Sitecore solution, you’ll notice that there is already a LinkManager.ascx file there, however, when i configured Sitecore to use that file, it was different than the embedded resource. By default, Sitecore uses the embedded resource and not the file on the file system, so I am making a copy of the embedded resource and we are going to modify that.

Modify the EditorPage.aspx

In your installed solution, navigate to this directory. sitecore\shell\Controls\Rich Text Editor. Find the file called “EditorPage.aspx”. Copy that file and place it in your solution as shown in the above solution screenshot.

Edit this file and configure the ExternalDialogsPath as shown. This tells Sitecore/Telerik where to look for our LinkManager.ascx control.

Edit the LinkManager.ascx

Now that we have the RichText control pointing to our LinkManager.ascx, we need to modify it. We will start by registering a new ascx control to encapsulate our modifications.

<%@ Register Src="~/sitecore/shell/Controls/Rich Text Editor/Dialogs/SearchXYZ.ascx" TagName="SearchXYZ" TagPrefix="nv" %>

The javascript dialog has 3 tabs. To make this the least invasive as possible to the Sitecore code, I’ve optioned to create a new user control to encapsulate the custom code I am creating. The control that is defined above is now going to be placed at the very bottom of the first tab.

<nv:SearchXYZ ID="SearchXYZ" runat="server" />

Code the overlay

Here is the code for the “SearchXYZ.ascx”

<%@ Control Language="C#" %>
<style>
    .hide {
        display: none;
    }

    .show {
        display: block;
    }

    .search-overlay {
        background-color: white;
        position: absolute;
        top: 10px;
        left: 0px;
        width: 400px;
        height: 400px;
        overflow: auto;
        z-index:1;
    }

    .search-overlay-results {
        border-collapse: collapse;
        width: 100%;
    }

        .search-overlay-results td, .search-overlay-results th {
            border: 1px solid #ccc;
            padding: 8px;
        }

        .search-overlay-results tr:nth-child(even) {
            background-color: #f2f2f2;
        }

        .search-overlay-results tr:hover {
            background-color: #ddd;
            cursor: pointer;
        }

        .search-overlay-results th {
            padding-top: 12px;
            padding-bottom: 12px;
            text-align: left;
            background-color: #5E5E5E;
            color: white;
        }

    .search-overlay-close-image {
        cursor: pointer;
        position: absolute;
        right: 0px;
        height: 20px;
        width: 20px;
        margin: 6px;
    }

    .search-overlay-header {
        width: 100%;
        height: 32px;
    }
</style>

<script type="text/javascript">
    function SearchXYZ() {
        this.SearchOverlay = document.getElementById("SearchOverlay");
        this.ShowOverlayButton = document.getElementById("btnShowSearchOverlay");
        this.CloseOverlayButton = document.getElementById("btnCloseSearchOverlay");

        this.SearchButton = document.getElementById("btnSearch");
        this.SearchTermInput = document.getElementById("txtSearchTerm");
        this.SearchResultsTable = document.getElementById("SearchResults");

        this.LinkUrl = document.getElementById("LinkURL");
    }

    SearchXYZ.prototype.Init = function () {
        var _this = this;

        this.ShowOverlayButton.addEventListener("click", function () {
            _this.ShowSearchOverlayOnClick();
        });

        this.CloseOverlayButton.addEventListener("click", function () {
            _this.CloseSearchOverlayOnClick();
        });

        this.SearchButton.addEventListener("click", function (e) {
            _this.SearchButtonOverlayOnClick();
            e.preventDefault();
        });
    }

    SearchXYZ.prototype.ShowSearchOverlayOnClick = function () {
        this.SearchOverlay.classList.remove("hide");
        this.SearchOverlay.classList.add("show");
    }

    SearchXYZ.prototype.CloseSearchOverlayOnClick = function () {
        this._closeSearchOverlay();
    }

    SearchXYZ.prototype.SearchButtonOverlayOnClick = function () {
        var term = this.SearchTermInput.value;
        this._searchApi(term);
    }

    SearchXYZ.prototype._closeSearchOverlay = function () {
        this.SearchOverlay.classList.remove("show");
        this.SearchOverlay.classList.add("hide");
        this._clearSearchOverlay();
    }

    SearchXYZ.prototype._clearSearchOverlay = function () {
        this.SearchTermInput.value = "";
        this._clearSearchResultsTable();
    }

    SearchXYZ.prototype._updateLinkDestination = function (link) {
        this.LinkUrl.value = link;
        this._closeSearchOverlay();
    }

    SearchXYZ.prototype._updateSearchResults = function (data) {
        _this = this;

        this._clearSearchResultsTable();

        var results = JSON.parse(data);
        let thead = this.SearchResultsTable.createTHead();
        let row = thead.insertRow();
        let th1 = document.createElement("th");
        let th2 = document.createElement("th");
        row.appendChild(th1);
        row.appendChild(th2);
        let text1 = document.createTextNode("Name");
        th1.appendChild(text1);
        let text2 = document.createTextNode("Url");
        th2.appendChild(text2);

        for (var i = 0; i < results.length; i++) {
            let row = this.SearchResultsTable.insertRow();
            let td1 = document.createElement("td");
            let td2 = document.createElement("td");
            let text1 = document.createTextNode(results[i].name);
            let text2 = document.createTextNode(results[i].url);
            td1.appendChild(text1);
            td2.appendChild(text2);
            row.appendChild(td1);
            row.appendChild(td2);
            row.classList.add("search-result-row");
            let url = results[i].url;

            row.addEventListener("dblclick", function () {
                _this._updateLinkDestination(url);
            });
        }
    }

    SearchXYZ.prototype._clearSearchResultsTable = function () {
        this.SearchResultsTable.innerHTML = "";
    }

    SearchXYZ.prototype._searchApi = function (term) {
        var _this = this;
        var url = "/api/SearchXYZ/search?term=" + term;

        fetch(url)
            .then(response => response.text())
            .then(data => {
                console.log(data);
                _this._updateSearchResults(data);
            });
    }

</script>

<p>
    <a href="#" id="btnShowSearchOverlay">Search XYZ</a>
</p>

<div id="SearchOverlay" class="search-overlay hide">
    <div id="searchHeader" class="search-overlay-header">
        <img src="/sitecore/shell/client/Applications/UpdateCenter/assets/images/close_w_bg.png" id="btnCloseSearchOverlay" class="search-overlay-close-image" />
    </div>
    <table>
        <tr>
            <td>Search Term:</td>
            <td>
                <input type="text" id="txtSearchTerm"></td>
        </tr>
        <tr>
            <td>Last name</td>
            <td>
                <button id="btnSearch">Search</button>
        </tr>
    </table>
    <table id="SearchResults" class="search-overlay-results">
    </table>
</div>

<script type="text/javascript">
    var search = new SearchXYZ();
    search.Init();
</script>

Create the API

In part 1, I defined a mock service layer to return the data from our search. For the javascript portion of this, we want to use the same service. In order to do this, we need a controller defined to call from our Ajax. I will run through that quickly.

Define the RouteConfig

using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace SampleSite.App_Start
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.MapRoute(
                name: "Api",
                url: "api/{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

Configure Sitecore to process the route configuration.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="SampleSite.Pipelines.LoadRoutes,SampleSite" patch:after="processor[@type='Sitecore.Pipelines.Loader.EnsureAnonymousUsers, Sitecore.Kernel']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Create the controller

using Newtonsoft.Json;
using SampleSite.ServiceLayer.XYZPlatform;
using SampleSite.Utilities;
using System.Web.Mvc;

namespace SampleSite.Controllers
{
    public class SearchXYZController : Controller
    {
        public SearchXYZController()
        {
        }

        public ActionResult Search(string term)
        {
            var data = XYZPlatformService.Search(term);
            return new JsonNetResult(data, Formatting.Indented);
        }
    }
}

About Phil Paris

Hi, my name is Phil Paris and I’m a Sitecore Architect and general Sitecore enthusiast. I’ve been working with Sitecore since 2013. Through this blog I will be sharing Sitecore insights, tips and tricks, best practices and general knowledge with the hopes to further the community at large. Please feel free to reach out to me at any time!

View all posts by Phil Paris →