Friday, June 14, 2013

How To: SharePoint Suggestions Box using Search Service and the Client Side Object Model

I recently had the requirement to have a suggestion box beneath the Title field on a new item form for a SharePoint list. 



Basically when a user submits a new item to a list, as he is typing in the title we want to display all other items in that list with similar names. Reason for this being people are submitting Ideas to the system and we don't want multiple ideas with the same purpose.

I initially used the standard SharePoint Lists service to get a list of items and then tried to find matching words - but I found that using the SharePoint search service does a better job in matching results.

I found a blog post on an async search box which does most of what I need by Jan Tielens (http://weblogs.asp.net/jan) and just made some adjustments. Below is the code I used on my page:

HTML---------------------------------------------------------------------------------------
<div id="quickSearchResults" style="display: none;"></div>

<style type="text/css">
#quickSearchResultsHeader{
line-height:20px;
font-size:16px;
}
#quickSearchResults{
border:solid 1px gray;
padding:10px;
line-height:20px;
}
</style>

JavaScript
---------------------------------------------------------------------------------------
// *** Customizable parameters ***
var quickSearchConfig = {
    delay: 500, // time to wait before executing the query (in ms)
    minCharacters: 3, // minimum nr of characters to enter before search
    numberOfResults: 5, // number of results to show
    resultsAnimation: 200, // animation time (in ms) of the search results
    resultAnimation: 0 // animation time (in ms) of individual result (when selected)
};
var quickSearchTimer;
var quickSearchSelectedDivIndex = -1;
var searchingTextBox = $("input[title='Title']");
$(document).ready(function () {
    searchingTextBox.keyup(function (event) {
        var previousSelected = quickSearchSelectedDivIndex;
        // catch some keys
        switch (event.keyCode) {
            case 13: // enter
                var selectedDiv = $("#quickSearchResults>div:eq(" + quickSearchSelectedDivIndex + ") a");
                if (selectedDiv.length == 1)
                    window.location = selectedDiv.attr("href");
                break;
            case 38: // key up
                quickSearchSelectedDivIndex--;
                break;
            case 40: // key down
                quickSearchSelectedDivIndex++;
                break;
        }
        // check bounds
        if (quickSearchSelectedDivIndex != previousSelected) {
            if (quickSearchSelectedDivIndex < 0)
                quickSearchSelectedDivIndex = 0;
            if (quickSearchSelectedDivIndex >= $("#quickSearchResults>div").length - 1)
                quickSearchSelectedDivIndex = $("#quickSearchResults>div").length - 2;
        }
        // select new div, unselect the previous selected
        if (quickSearchSelectedDivIndex > -1) {
            if (quickSearchSelectedDivIndex != previousSelected) {
                unSelectDiv($("#quickSearchResults>div:eq(" + previousSelected + ")"));
                selectDiv($("#quickSearchResults>div:eq(" + quickSearchSelectedDivIndex + ")"));
            }
        }
        // if the query is different from the previous one, search again
        if (searchingTextBox.data("query") != searchingTextBox.val()) {
            if (quickSearchTimer != null) // cancel the delayed event
                clearTimeout(quickSearchTimer);
            quickSearchTimer = setTimeout(function () { // delay the searching
                $("#quickSearchResults").fadeOut(200, initSearch);
            }, quickSearchConfig.delay);
        }
    });
});
function unSelectDiv(div) {
    // first stop all animations still in progress
    $("#quickSearchResults>div>div").stop(true, true);
    div.removeClass("quickSearchResultDivSelected").addClass("quickSearchResultDivUnselected");
    $("#details", div).hide();
}
function selectDiv(div) {
    div.addClass("quickSearchResultDivSelected");
    $("#details", div).slideDown(quickSearchConfig.resultAnimation);
}
function initSearch() {
    // first store query in data
    searchingTextBox.data("query", searchingTextBox.val());
    // clear the results
    $("#quickSearchResults").empty();
    // start the search
    var query = searchingTextBox.val();
    if (query.length >= quickSearchConfig.minCharacters) {
        search(query);
    }
}
function search(query) {
    quickSearchSelectedDivIndex = -1;
    var queryXML =
"<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'> \
<Query domain='QDomain'> \
<SupportedFormats><Format>urn:Microsoft.Search.Response.Document.Document</Format></SupportedFormats> \
<Context> \
<QueryText language='en-US' type='STRING' >contenttype:Idea " + query + "</QueryText> \
</Context> \
<SortByProperties><SortByProperty name='Rank' direction='Descending' order='1'/></SortByProperties> \
<Range><StartAt>1</StartAt><Count>" + quickSearchConfig.numberOfResults + "</Count></Range> \
<EnableStemming>false</EnableStemming> \
<TrimDuplicates>true</TrimDuplicates> \
<IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery> \
<ImplicitAndBehavior>true</ImplicitAndBehavior> \
<IncludeRelevanceResults>true</IncludeRelevanceResults> \
<IncludeSpecialTermResults>true</IncludeSpecialTermResults> \
<IncludeHighConfidenceResults>true</IncludeHighConfidenceResults> \
</Query></QueryPacket>";
    var soapEnv =
"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> \
<soap:Body> \
<Query xmlns='urn:Microsoft.Search'> \
<queryXml>" + escapeHTML(queryXML) + "</queryXml> \
</Query> \
</soap:Body> \
</soap:Envelope>";
    $.ajax({
        url: "/_vti_bin/search.asmx",
        type: "POST",
        dataType: "xml",
        data: soapEnv,
        complete: processResult,
        contentType: "text/xml; charset=\"utf-8\""
    });
    function processResult(xData, status) {
        var xml = "<xml>" + $(xData.responseText).find("QueryResult").text() + "</xml>";
        var xmldoc = new ActiveXObject("Microsoft.XMLDOM");
        xmldoc.loadXML(xml);
        var itemElements = xmldoc.getElementsByTagName("Document");
        var srHtml = "";
        if (itemElements.length > 0) {
            srHtml = "<div id=\"quickSearchResultsHeader\">Similar Ideas</div><div>";
            for (var i = 0; i < itemElements.length; i++) {
                var srUrl = $(itemElements[i].getElementsByTagName("LinkUrl")).text();
                if (srUrl.indexOf('/Lists/Ideas') > 0) { // limits the results to one specific list
                    var srTitle = $(itemElements[i].getElementsByTagName("Title")).text();
                    srHtml = srHtml + "<a href=\"" + srUrl + "\">" + srTitle + "</a><br/>";
                }
            }
            srHtml = srHtml + "</div>";
            $("#quickSearchResults").html(srHtml);
            $("#quickSearchResults").slideDown(200);
        }
    }
}
function escapeHTML(str) {
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}