Single Select with Search field in Sitecore

With Sitecore 7.2 we have the new sitecore fields with search available.
The multilist with search is a real life saver when it comes to usage of Sitecore buckets.
By default the Sitecore buckets create a hierarchy of folders with the
Year -> Month -> Day -> Hour -> Minute structure.

2015-02-25_002948

Makes it awfully hard to find items without a good search system in place.

Which is why the multilist with search brings us a sigh of relief. However, in one of our projects, we found the need to be able to restrict the selected number of items to 1 only – like in the case of selecting a featured product etc.

Initially we started off by just taking the first item selected in the multilist with search field, while we processed data in the back end. However, this opened a can of worms when it came to training the content authors. Which is why, we sought an alternate solution, and created a custom field which allows only a single selection with search as is.

This is what it now looks like:

2015-02-25_004829

We also added a validation message to be displayed if a user tried to add an additional selected item.

2015-02-25_004948


Here’s how we did it –

There are 4 parts to this solution:

  • C# code file for the new control – most of the code in this file actually comes from the reflected code for multilist with search. We however did make a few updates for changing the look and feel of the control – Mainly removing the sort icons, move left / right icons and to arrange the fields one below the other, with the ‘selected’ box – as tall as a single item name text.
  • JS file for additional validations & CSS file for a small style update – again, this is the multilist with search js / css brought over and modified for the additional validation of allowing only 1 item to be selected.
  • Item added to the core database – to make the new field available in sitecore.
  • Config update – to add the extension of the new control created.

C# code file
(Refer Sitecore.Buckets.FieldTypes.BucketList)

2015-02-25_013640

using Sitecore;
using Sitecore.Buckets.FieldTypes;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Web.UI;

namespace MySite.SitecoreFields
{
    public class SingleSelectWithSearchField : BucketList
    {
        protected override void DoRender(HtmlTextWriter output)
        {
            ArrayList selected;
            OrderedDictionary unselected;
            GetSelectedItems(GetItems(Sitecore.Context.ContentDatabase.GetItem(ItemID)), out selected, out unselected);
            StringBuilder stringBuilder = new StringBuilder();
            foreach (DictionaryEntry dictionaryEntry in unselected)
            {
                Item obj = dictionaryEntry.Value as Item;
                if (obj != null)
                {
                    stringBuilder.Append(obj.DisplayName + ",");
                    stringBuilder.Append(GetItemValue(obj) + ",");
                }
            }
            RenderStartLocationInput(output);
            output.Write("<input type='hidden' width='100%' id='multilistValues" + (object)ClientID + "' value='" + stringBuilder + "' style='width: 200px;margin-left:3px;'>");
            ServerProperties["ID"] = ID;
            string str1 = string.Empty;
            if (ReadOnly)
                str1 = " disabled='disabled'";
            output.Write("<input id='" + ID + "_Value' type='hidden' value='" + StringUtil.EscapeQuote(Value) + "' />");
            output.Write("<table" + GetControlAttributes() + ">");
            output.Write("<tr>");
            output.Write("<td class='scContentControlMultilistCaption' width='50%' colspan='4'>" + Translate.Text("All") + "</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign='top' height='100%' colspan='4'>");
            output.Write("<div style='width:200%;overflow:hidden;height:30px'><input type='text' width='100%' class='scIgnoreModified bucketSearch inactive' value='" + TypeHereToSearch + "' id='filterBox" + ClientID + "' " + (Sitecore.Context.ContentDatabase.GetItem(ItemID).Access.CanWrite() ? string.Empty : "disabled") + ">");
            output.Write("<span id='prev" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/right.png' style='margin-top: 1px;'> " + Translate.Text("prev") + " |</span>");
            output.Write("<span id='next" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("next") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/left.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span id='refresh" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("refresh") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/refresh.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span id='goto" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("go to item") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/text.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span style='padding-left:34px;'><strong>" + Translate.Text("Page Number") + ": </strong></span><span id='pageNumber" + ClientID + "'></span></div>");
            string str2 = !UIUtil.IsIE() || UIUtil.GetBrowserMajorVersion() != 9 ? "10" : "11";
            output.Write("<select id=\"" + ID + "_unselected\" class=\"scContentControlMultilistBox\" size=\"" + str2 + "\"" + str1 + " >");
            foreach (DictionaryEntry dictionaryEntry in unselected)
            {
                Item obj = dictionaryEntry.Value as Item;
                if (obj != null)
                {
                    string str3 = OutputString(obj);
                    output.Write("<option value='" + GetItemValue(obj) + "'>" + str3 + "</option>");
                }
            }
            output.Write("</select>");
            output.Write("</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td class='scContentControlMultilistCaption' width='100%'>Selected</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign='top' height='100%' colspan='4'>");
            output.Write("<select id='" + ID + "_selected' class='scContentControlMultilistBox scSingleSelectWithSearchSelectedBox' size='10'" + str1 + ">");
            for (int index = 0; index < selected.Count; ++index)
            {
                Item obj1 = selected[index] as Item;
                if (obj1 != null)
                {
                    string str3 = OutputString(obj1);
                    output.Write("<option value='" + GetItemValue(obj1) + "'>" + str3 + "</option>");
                }
                else
                {
                    string path = selected[index] as string;
                    if (path != null)
                    {
                        Item obj2 = Sitecore.Context.ContentDatabase.GetItem(path);
                        string str3 = obj2 == null ? path + ' ' + Translate.Text("[Item not found]") : OutputString(obj2);
                        output.Write("<option value='" + path + "'>" + str3 + "</option>");
                    }
                }
            }
            output.Write("</select>");
            output.Write("</td>");
            output.Write("</tr>");
            output.Write("<div style='border:1px solid #999999;font:8pt tahoma;display:none;padding:2px;margin:4px 0px 4px 0px;height:14px' id='" + ID + "_all_help'></div>");
            output.Write("<div style='border:1px solid #999999;font:8pt tahoma;display:none;padding:2px;margin:4px 0px 4px 0px;height:14px' id='" + ID + "_selected_help'></div>");
            output.Write("</table>");
            RenderScript(output);
        }

        protected override void RenderScript(HtmlTextWriter output)
        {
            string str = "<script type='text/javascript'>\r\n                                    (function() {\r\n                                        if (!document.getElementById('SingleSelectWithSearchJs')) {\r\n                                            var head = document.getElementsByTagName('head')[0];\r\n                                            head.appendChild(new Element('script', { type: 'text/javascript', src: '/sitecore/shell/Controls/SingleSelectWithSearch/SingleSelectWithSearch.js', id: 'SingleSelectWithSearchJs' }));\r\n                                            head.appendChild(new Element('link', { rel: 'stylesheet', href: '/sitecore/shell/Controls/SingleSelectWithSearch/SingleSelectWithSearch.css' }));\r\n                                        }\r\n                                        var stopAt = Date.now() + 5000;\r\n                                        var timeoutId = setTimeout(function() {\r\n                                            if (Sitecore.InitSingleSelectWithSearch) {\r\n                                                Sitecore.InitSingleSelectWithSearch(" + ScriptParameters + ");\r\n                                                clearTimeout(timeoutId);\r\n                                            } else if (Date.now() > stopAt) {\r\n                                                clearTimeout(timeoutId);\r\n                                            }\r\n                                        }, 100);\r\n                                    }());\r\n                              </script>";
            output.Write(str);
        }
    }
}

JS File
(Refer ~\sitecore\shell\Controls\BucketList\BucketList.js
To be saved at: ~\sitecore\shell\Controls\SingleSelectWithSearch\SingleSelectWithSearch.js

var Sitecore = Sitecore || {};

Sitecore.InitSingleSelectWithSearch = function (id, clientId, pageNumber, searchHandlerUrl, filter, databaseUrlParameter, typeToSearchString, of, enableSetStartLocation) {
    var self = {};

    self.id = id;
    self.clientId = clientId;
    self.pageNumber = pageNumber;
    self.searchHandlerUrl = searchHandlerUrl;
    self.filter = filter;
    self.databaseUrlParameter = databaseUrlParameter;
    self.typeToSearchString = typeToSearchString;
    self.of = of;
    self.enableSetStartLocation = (enableSetStartLocation.toLowerCase() === 'true');

    self.currentPage = 1;
    self.selectedId = '';

    self.doneTypingInterval = 2000; //time in ms, 2 second for example

    var typingTimer;

    self.format = function (template) {
        var args = arguments;
        return template.replace(/\{(\d+)\}/g, function (m, n) { return args[parseInt(n) + 1]; });
    };

    // Sends 'GET' request to url specified by parameter
    // and apply success handler to multilist element
    self.sendRequest = function (url, multilist) {
        new Ajax.Request(url,
            {
                method: 'GET',
                onSuccess: new self.SuccessHandler(multilist)
            });
    };

    // Cunstructor for request success handler
    self.SuccessHandler = function (multilist) {
        return function (request) {
            var response = eval(request.responseText);
            multilist.options.length = 0;
            multilist.removeClassName('loadingItems');

            for (var i = 0; i < response.items.length; i++) {
                multilist.options[multilist.options.length] = new Option(response.items[i].Name + ' (' + response.items[i].TemplateName + ' - ' + response.items[i].Bucket + ')', response.items[i].ItemId);
            }

            self.pageNumber = response.PageNumbers;
            $('pageNumber' + self.clientId).innerHTML = self.format(self.of, self.currentPage, self.pageNumber);
        };
    };

    // Return id of selected item
    self.getSelectedItemId = function () {
        var all = scForm.browser.getControl(self.id + '_unselected');

        for (var n = 0; n < all.options.length; n++) {
            var option = all.options[n];

            if (option.selected) {
                return option.value;
            }
        }

        return null;
    };

    self.onFilterFocus = function (filterBox) {
        if (filterBox.value == self.typeToSearchString) {
            filterBox.value = '';
        }

        filterBox.addClassName('active').removeClassName('inactive');
    };

    self.onFilterBlur = function (filterBox) {
        if (!filterBox.value) {
            filterBox.value = self.typeToSearchString;
        }

        filterBox.removeClassName('active').addClassName('inactive');
    };

    self.multilistValuesMoveRight = function (allOptions) {
        var all = scForm.browser.getControl(self.id + '_unselected');
        var multilistValues = document.getElementById('multilistValues' + self.id);
        for (var n = 0; n < all.options.length; n++) {
            var option = all.options[n];
            if (option.selected || allOptions) {
                var opt = option.innerHTML + ',' + option.value + ',';
                multilistValues.value = multilistValues.value.replace(opt, '');
            }
        }
    };

    self.multilistValuesMoveLeft = function (allOptions) {
        var selected = scForm.browser.getControl(self.id + '_selected');
        var multilistValues = document.getElementById('multilistValues' + self.id);
        for (var n = 0; n < selected.options.length; n++) {
            var option = selected.options[n];
            if (option.selected || allOptions) {
                var opt = option.innerHTML + ',' + option.value + ',';
                multilistValues.value += opt;
            }
        }
    };

    self.moveToCurrentPage = function () {
        var filterBox = document.getElementById('filterBox' + self.clientId);
        var filterValue = (filterBox.value && filterBox.value != self.typeToSearchString) ? filterBox.value : '*';

        var multilist = $(self.clientId + '_unselected').addClassName('loadingItems');
        var savedStr = encodeURI(filterValue);
        var filterString = self.enableSetStartLocation ? self.getOverrideString('&location=') : self.filter;

        self.sendRequest(self.searchHandlerUrl + '?fromBucketListField=' + savedStr + filterString + '&pageNumber=' + self.currentPage + self.databaseUrlParameter, multilist);
    };

    // Replaces overrideKey value in filter by value from ovverrideInput
    self.getOverrideString = function (overrideKey) {
        var overrideInput = document.getElementById('locationOverride' + self.clientId);

        if (!overrideInput || !overrideInput.value.length > 0) {
            return self.filter;
        }

        var replaceStartIndex = self.filter.indexOf(overrideKey);

        if (!~replaceStartIndex) {
            return self.filter;
        }

        var replaceEndIndex = self.filter.indexOf('&', replaceStartIndex + 1);

        if (!~replaceEndIndex) {
            replaceEndIndex = self.filter.length;
        }

        var stringToReplace = self.filter.substring(replaceStartIndex, replaceEndIndex);

        return self.filter.replace(stringToReplace, overrideKey + overrideInput.value);
    };

    self.initEventHandlers = function () {
        $('filterBox' + self.clientId).observe('focus', function () {
            self.onFilterFocus($('filterBox' + self.clientId));
        });

        $('filterBox' + self.clientId).observe('blur', function () {
            self.onFilterBlur($('filterBox' + self.clientId));
        });

        $('filterBox' + self.clientId).observe('keyup', function () {
            typingTimer = setTimeout(function () { self.currentPage = 1; self.moveToCurrentPage(); }, self.doneTypingInterval);
        });

        $('filterBox' + self.clientId).observe('keydown', function () {
            clearTimeout(typingTimer);
        });

        $('next' + self.clientId).observe('click', function () {
            if (self.currentPage + 1 <= self.pageNumber) {
                self.currentPage++;
                self.moveToCurrentPage();
            }
        });

        $('prev' + self.clientId).observe('click', function () {
            if (self.currentPage > 1) {
                self.currentPage--;
                self.moveToCurrentPage();
            }
        });

        $(self.id + '_unselected').observe('dblclick', function () {
			if(jQuery('#'+self.id+'_selected').find('option').length==0)
			{
				self.multilistValuesMoveRight();
				javascript: scContent.multilistMoveRight(self.id);
			}
			else if(jQuery('#'+self.id+'_selected').find('option').length==1)
			{
				alert('Only one item is allowed here. Please remove the selected item to add a new item.');
			}
        });

        $(self.id + '_selected').observe('dblclick', function () {
            self.multilistValuesMoveLeft();
            javascript: scContent.multilistMoveLeft(self.id);
        });

        $(self.id + '_unselected').observe('click', function () {
            self.selectedId = self.getSelectedItemId();
        });

        $(self.id + '_selected').observe('click', function () {
            self.selectedId = self.getSelectedItemId();
        });

        $('refresh' + self.clientId).observe('click', function () {
            self.currentPage = 1;
            self.moveToCurrentPage();
        });

        $('goto' + self.clientId).observe('click', function () {
            scForm.postRequest('', '', '', 'contenteditor:launchtab(url=' + self.selectedId + ')');
            return false;
        });
    };

    $('pageNumber' + self.clientId).innerHTML = self.format(self.of, self.currentPage, self.pageNumber);
    self.initEventHandlers();
};

CSS Update
To be saved at: ~\sitecore\shell\Controls\SingleSelectWithSearch\SingleSelectWithSearch.css

.loadingItems
{
    background-image: url('/sitecore/shell/Applications/Buckets/images/load.gif');
    background-position: 50%;
    background-repeat: no-repeat;
}

.bucketSearch.active
{
    color: black;
}

.bucketSearch.inactive
{
    color: gray;
}

.scSingleSelectWithSearchSelectedBox
{
    margin-top: 3px;
    height: 19px;
}

Core DB Update

Add an item in the content tree of the core database at:

2015-02-25_013159

The extension will be registered in the sitecore configuration in the next step. The extension is followed by the class name we used in the step above.

Sitecore configuration update

  <sitecore>
    <!-- New control added - Single Select with Search -->
    <controlSources>
      <source mode="on" namespace="MySite.SitecoreFields" assembly="MySite" prefix="contentExtension" />
    </controlSources>
  </sitecore>

That’s it! You should be all set, and the new control should now appear in the list of available list of controls in sitecore:

2015-02-25_015056

Also, the datasource format will remain the same as with Multilist with Search, example:

StartSearchLocation={20265CCE-B2FF-472C-AA85-9375A313F239}&TemplateFilter={18538774-A45B-43A4-94BD-DC73A0C8FBF9}|{D98FC013-5AF4-4F09-A405-F2A52B6AB8D5}&Filter=_language:en

One thought on “Single Select with Search field in Sitecore

Leave a comment