Editor to create image hotspots in Sitecore

We had the need to create a module which showed an image with hotspots on it – basically positional tooltips.

We created subitems of the module for each hotspot, and expected the content author to provide the position of the hotspot on the module image – in percentage – relative to the top / left of the image.
Now this posed an issue for the content authors since they had to either guess the position of each hotspot or upload the image onto a different online tool and then get the position from there to add into out hotspot sub items.

To remedy this we created a tool and integrated it with the Sitecore Content Editor – which would allow the content author to just click on the points on the image and create the corresponding hotspots.

To make this tool more versatile, we have made it configurable, so it can be used with any instance of Sitecore and any module, as long as it is configured in the hotspot specific configuration xml.

Here’s an example of the end result:

2016-03-22_191037

2016-03-22_190113

To achieve this, we needed to implement the following steps:

  1. Create an aspx page in your solution which Sitecore will load as a tab in the content editor.2016-03-22_210644
  2. Create an Editor item in Core DB, which will point to the aspx page you created.
  3. Set the Editor item on the standard values item of the template which would have the image on which the hotspots need to be set.
  4. Add the relevant code in the aspx page.

Create an aspx page in your solution

In our solution, we added the aspx page in the following location:

2016-03-22_220705

Create an Editor item in Core DB

2016-03-22_220900

Set the Editor item on the standard values item of the template

In this example, we have a POI module datasource template – which will have the image field for which we want to create the hotspots / points of interest. So we will set this editor in the standard values of this datasource template.

2016-03-22_221350.png

ASPX page code

We have the back end and front end code here. From a usability perspective, the current code only enables the content author to create new hotpot / point of interest items. Editing / deleting hotpot / point of interest items can be done directly at the individual hotspot level. Additionally, to make it convenient for a content author to identify which hotspot they want to edit / delete, on load of the tab, we have front end code, which shows the current set of hotspots that already exist as sub items.

While the top / left coordinate % is auto populated by this tool, we have an optional field for the tooltip description which a content author can choose to populate before creating the hotspot.

Futher, in our project, we have tried to make this tool as configurable as possible! So now, we can use this tool on any module, by making sitecore and config updates!

So here’s the code we used:

Aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Hotspots.aspx.cs" Inherits="shared.sitecore_modules.Shell.Editors.ProductResourceHotspot.Hotspots" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Hotspots</title>
    <script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
    <script src="./hotspots.js"></script>
    <link type="text/css" rel="stylesheet" href="./hotspot.css" />
</head>
<body>
    <form id="formHotSpot" runat="server">
        <div data-hotspot='<%=HotSpotsJson %>' class="hotspot-image-wrapper">
            <asp:Image ID="imgResource" runat="server" CssClass="resource-image" />
            <br />
            <asp:Label ID="lblNoImageFound" runat="server" CssClass="not-available">
                Image not found.
            </asp:Label>
            <div id="existingHotspots"></div>
        </div>
        <table id="tableHotSpots">
        </table>
        <input type="hidden" id="hotspotCounter" name="hotspotCounter" />
        <asp:HiddenField runat="server" ID="hiddenHotspotTemplateId" />
        <asp:Button ID="btnSubmit" runat="server" Text="Create Hotspots" OnClick="btnSubmit_Click" CssClass="submit" />
    </form>
</body>
</html>

You’ll see some additional labels etc above, to show alerts to the content author (clearly, I am way too used to MVC now and have completely forgotten how to make nice in asp.net! Please excuse!)

Code Behind

Sitecore actually loads up our aspx page in an iframe, passing in the item id in a querystring parameter, which we use below.

namespace shared.sitecore_modules.Shell.Editors.ProductResourceHotspot
{
    public partial class Hotspots : System.Web.UI.Page
    {
        public string HotSpotsJson { get; set; }

        public hotspot HotspotTemplateInfo { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            LoadHotspotTemplateInfo();

            string itemId = Request.QueryString["id"];
            if (itemId != null)
            {
                Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

                hotspotHotspottemplate currentTemplateHotspotSetting = HotspotTemplateInfo.Items
                    .FirstOrDefault(x => x.sourcetemplate[0].Value == currentItem.TemplateID.ToString());

                if (currentTemplateHotspotSetting != null)
                {
                    Field rawField = currentItem.Fields[currentTemplateHotspotSetting.sourceimagefieldname[0].Value];
                    bool fieldIsValidAndHasContent = rawField != null && !string.IsNullOrEmpty(rawField.Value) &&
                                                    FieldTypeManager.GetField(rawField) is ImageField;
                    if (fieldIsValidAndHasContent)
                    {
                        ImageField imageField = rawField;
                        if (imageField.MediaItem != null)
                        {
                            var mediaUrlOptions = new MediaUrlOptions { UseItemPath = false, AbsolutePath = true, Database = Factory.GetDatabase("master") };
                            string imageUrl = MediaManager.GetMediaUrl(imageField.MediaItem, mediaUrlOptions);

                            if (!string.IsNullOrWhiteSpace(imageUrl))
                            {
                                imgResource.Visible = true;
                                lblNoImageFound.Visible = false;
                                imgResource.ImageUrl = Request.Url.Scheme + "://" + Request.Url.Host + imageUrl;
                            }
                            else
                            {
                                imgResource.Visible = false;
                                lblNoImageFound.Visible = true;
                            }
                        }
                        else
                        {
                            imgResource.Visible = false;
                            lblNoImageFound.Visible = true;
                        }
                    }
                    else
                    {
                        imgResource.Visible = false;
                        lblNoImageFound.Visible = true;
                    }

                    hiddenHotspotTemplateId.Value = currentTemplateHotspotSetting.hotspotitemtemplate[0].Value;

                    if (imgResource.Visible)
                    {
                        List<Item> currentHotspots = currentItem.HasChildren
                            ? currentItem.GetChildren().InnerChildren
                                .Where(x => x.TemplateID.ToString() == currentTemplateHotspotSetting.hotspotitemtemplate[0].Value).ToList()
                            : null;

                        if (currentHotspots != null && currentHotspots.Any())
                        {
                            HotSpotsJson = new JavaScriptSerializer().Serialize(new
                            {
                                HotSpots = currentHotspots.Select(x => new
                                {
                                    Description = x.Name + ": "
                                                  + SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Description", x, false),
                                    Left = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Left Coordinate", x, false),
                                    Top = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Right Coordinate", x, false)
                                })
                            });
                        }
                    }
                }
            }
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            int? hotspotCounter = Request.Form["hotspotCounter"].SafeToInt();
            string itemId = Request.QueryString["id"];
            Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

            if (hotspotCounter != null)
            {
                using (new SecurityDisabler())
                {
                    for (int i = 1; i <= hotspotCounter; i++)
                    {
                        Item hotspotItem = AddUpdateHelper.CreateSitecoreItemUsingTemplate(currentItem.Paths.FullPath,
                            hiddenHotspotTemplateId.Value, "Hotspot" + DateTime.Now.ToString("ddhhmmssfff"));

                        hotspotItem.Editing.BeginEdit();
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Top_CoordinateFieldName].Value = Request.Form["txtTop_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Left_CoordinateFieldName].Value = Request.Form["txtLeft_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_DescriptionFieldName].Value = Request.Form["txtDesc_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_TitleFieldName].Value = "Hotspot" + i;
                        hotspotItem.Editing.EndEdit();
                    }
                }
            }
        }

        private void LoadHotspotTemplateInfo()
        {
            if (Application["HotspotTemplateInfo"] != null)
            {
                HotspotTemplateInfo = (hotspot)Application["HotspotTemplateInfo"];
                return;
            }

            XmlSerializer ser = new XmlSerializer(typeof(hotspot));
            using (XmlReader reader = XmlReader.Create(AppDomain.CurrentDomain.BaseDirectory
                + "\\sitecore modules\\Shell\\Editors\\Hotspots\\HotspotsTemplateData.xml"))
            {
                HotspotTemplateInfo = (hotspot)ser.Deserialize(reader);
                Application["HotspotTemplateInfo"] = HotspotTemplateInfo;
            }
        }
    }
}

XML Configuration

In the above code, you’ll see that we are reading some configuration from an xml file. What we are essentially doing in this xml file, is specifying by template, which field will contain the image that we need to load in the hotspot tab, and the hotspot item template to use. We are using the same base template for hotspots, which is why you see the field names hardcoded in the code above. You could however include these field names also in the xml configuration if your field names between templates are likely to change!

Here’s a sample of the xml in use:

<?xml version="1.0" encoding="utf-8" ?>

<hotspot>

  <hotspottemplate>
    <sourcetemplate hint="/sitecore/templates/User Defined/MySite/Components/Modules/Enhanced POI Module">{85FB47EE-CFD4-405F-84A7-B53BF46CDA25}</sourcetemplate>
    <sourceimagefieldname hint="">Background Image</sourceimagefieldname>
    <hotspotitemtemplate hint="/sitecore/templates/User Defined/MySite/Components/Data/Point Of Interest">{43FB275D-9157-48B3-A68E-F143F044B7EE}</hotspotitemtemplate>
  </hotspottemplate>

</hotspot>

Auto generated cs file for the XML config

I used xsd.exe from Visual Studio developer tools command prompt to generate the class (hotspot) for this xml. You could alternatively do this manually!

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.18444
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Xml.Serialization;

// 
// This source code was auto-generated by xsd, Version=4.0.30319.33440.
// 


/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public partial class hotspot {
    
    private hotspotHotspottemplate[] itemsField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspottemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public hotspotHotspottemplate[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplate {
    
    private hotspotHotspottemplateSourcetemplate[] sourcetemplateField;
    
    private hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldnameField;
    
    private hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplateField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourcetemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourcetemplate[] sourcetemplate {
        get {
            return this.sourcetemplateField;
        }
        set {
            this.sourcetemplateField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourceimagefieldname", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldname {
        get {
            return this.sourceimagefieldnameField;
        }
        set {
            this.sourceimagefieldnameField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspotitemtemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplate {
        get {
            return this.hotspotitemtemplateField;
        }
        set {
            this.hotspotitemtemplateField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourcetemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourceimagefieldname {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateHotspotitemtemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

Stylesheet

Following is the hotspot.css that was used (crude alert!)

.resource-image {
    max-height: 700px;
    max-width: 700px;
    border: 1px solid black;
    margin-left: 20px;
    margin-top: 20px;
}

.desc {
    width: 400px;
}

.dimension {
    width: 30px;
}

.submit {
    display: none;
}

.delete {
    width: 20px;
    height: 20px;
    background-repeat: no-repeat;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGeYUxB9wAAACBjSFJNAAB6JQAAgIMAAPn/AACA6AAAUggAARVYAAA6lwAAF2/XWh+QAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACoklEQVQ4T62V3UtTcRjHv8uXdNYUp2y+YJgQGGFYkgUqFXVldTWILvwLAm+CLIRICLwML7oJRLzzouiFUkMpnZSiOY/T1LmZ5Zpz5pxm0/m2p+ec83M69RCIH3g4v/M8z/me58fvyzk6YnCIHBFXhTDHpro8MJEJA55phCddWJ/3Y/NEHsxnCxCjtGizwRHo/oIVSYJzPoAr1dWqYMDtRoJjCIkba6BpNzZ4PbVGSHlSB2OSXn16B7LQbHs7FuueIpiZiT8lZSi2WJCUmKhu+ajRCNfMHILDA9AFZqBb/YuTji4sF2bj56hDbomwuLSE8Zvl8N+5je+FhUh7VIOrFRWKmII84RaOwSEau3+XFi9lUvDMMZrLiaFRbhl59UapuxobqZfvO5OP04emJiW3myhBmRWOsdZmsueZaTIG5NSDvrJIt8VCfXx9HxdLNptNbd6HPYJbLHD0P66hPh1ogIVsHG9zc2l8RX6lNlG22UkyR8aNcoRi45RDkO1k8HphWF+Xy5poCrrb2uAsKoKeBX4ZU+E3m5ERCmGw9LLo0EBMGsVwbS194lI/R4vBQA7e5irnrWWlFOSc9cE9tXEf9ghOvHxBHfyQfBCfOTpaWkSF/RpcpqEL+TSbCpqUJJGNJkrQ53TSRyE2zPG6slJUtvGFQuQygHrP5VNY5HYSJdhpMimnyVam1pws8on8bn5IA+TiHqn5nchsExGcslpJimcjJ3BjPPuuo0tU9sdeVUWDp/JoWdxvERGcqH9OnlvXyFuQRY5cPXl9WvOpbHJYkxLI3dOjJgQR21C6CSnF55FechHhLDP7TtNRCnI1u74B/pFvakKw/fniz8/vhmdI8zjQZzqN61UP/yOpIts8Tl2qKHMKFgILZJfsynYOyiH/AoB/WsAsVQ1Uw2EAAAAASUVORK5CYII=);
}

.hotspot-image-wrapper {
    position: relative;
    display: inline-block;
}

#existingHotspots span {
    position: absolute;
}

    #existingHotspots span:before {
        display: inline-block;
        background-color: yellow;
        width: 20px;
        height: 20px;
        content: "";
        border-radius: 50%;
    }

#tableHotSpots {
    margin-left: 20px;
    margin-top: 20px;
    font-family: sans-serif;
    font-size: 13px;
}

#btnSubmit {
    margin-top: 10px;
    margin-left: 210px;
    margin-bottom: 20px;
}

Javascript

And here’s the javascript – hotspot.js (another crude alert – using a template engine would be well advised here!)

$(document).ready(function () {
    var hotspotsobj = $("div[data-hotspot]").data("hotspot");

    if (hotspotsobj != "") {
        $.each(hotspotsobj.HotSpots, function(key, value) {
            $("#existingHotspots").append("<span style=\"left:" + value.Left + "%;top:" + value.Top + "%;\" title=\"" + value.Description + "\"></span>");
        });
    }

    $(document).on("click", ".delete", function() {
        if (confirm("Delete Hotspot?")) {
            $(this).closest("tr").remove();
            if ($("#tableHotSpots tr").length == 1) {
                $("#tableHotSpots tr").remove();
                $(".submit").hide();
            }
        }
    });

    $("#imgResource").click(function (e) {
        if ($(".not-available").length > 0) {
            return;
        }

        if ($(this).attr("src").value != "") {
            var offset_t = $(this).offset().top - $(window).scrollTop();
            var offset_l = $(this).offset().left - $(window).scrollLeft();

            var left = Math.round((e.clientX - offset_l));
            var top = Math.round((e.clientY - offset_t));

            var percentageLeft = Math.round((left / $(this).width()) * 100);
            var percentageTop = Math.round((top / $(this).height()) * 100);

            var hotspotCount = $("#tableHotSpots tr").length;

            if (hotspotCount == 0) {
                $("#tableHotSpots").append("<tr><td>Description</td><td>Left</td><td>Top</td><td></td></tr>");
                hotspotCount++;
                $(".submit").show();
            }

            $("#tableHotSpots").append("<tr><td><input class=\"desc\" name=\"txtDesc_" + hotspotCount + "\" type=\"text\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtLeft_" + hotspotCount + "\" value=\"" + percentageLeft + "\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtTop_" + hotspotCount + "\" value=\"" + percentageTop + "\"></input></td>"
                + "<td><div class=\"delete\"></div></td></tr>");

            $("#tableHotSpots tr:last-child td:first-child input").focus();

            $("#hotspotCounter").val(hotspotCount);
        }
    });
});
Advertisements

, , , , , , ,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: