Subscribe to News feed

Automatically add ‘Search As You Type’ to every SharePoint page using Infuser

Posted at: 5:10 PM on 30 July 2009 by Muhimbi

Inject some goodies into your site It’s only been a day since we launched our free Infuser tool  and we already have an excellent application for it; Add Search as you Type support to SharePoint’s standard search box.

As many of you will be aware, Jan Tielens created a proof of concept some time ago to demonstrate how you could add this functionality to a custom web part. Very innovative, but unfortunately it has a couple of problems, most notably:

  • The script has to be added manually to every page on the site, which is very laborious and not always possible.
     
  • It doesn’t integrate with the standard MOSS search box, leaving the end user with two search boxes to choose from.
     
  • If the user doesn’t have access to the root site of the portal then search doesn’t work.
     
  • Resizing the window with the search results causes undesired effects.
     
  • The search button is present, but doesn’t work.
     
  • It is not clear how to make the search results window disappear.
     
  • It relies on the MOSS Search web service and doesn’t work with WSS.  
     

    Although Jan’s script works without modification in Infuser, we asked Jan’s permission and spent a couple of hours to address the points listed above with the exception of WSS compatibility, feel free to add this yourself. The full source code is listed at the end of this article.  

SearchAsYouType

  Follow the steps outlined below to add Search as you Type to your site collection:

  1. Download and install Muhimbi’s SharePoint Infuser on one of your Web Front End Servers.
     
  2. Ensure you have Designer privileges, more specifically the Add and Customize Pages right.
     
  3. From the Site Actions / Site Settings screen, select Infuse custom content in the Look and Feel column.
     
  4. Download the code and paste it into the Infuser window. If copying from the window below and you are using IE then you may want to paste it in WordPad first , otherwise all line breaks are stripped out.
     
  5. Click the Save button and navigate to any page that contains a search box and start typing.
     

<style type="text/css">
    /* Hide the search scope fields as we don't take their input */
    .ms-sbscopes          {display:none}   /* MOSS */
    #idSearchScope        {display:none}   /* WSS */
 
    .quickSearchResultDivUnselected
    {
        background: white;
        border: 1px solid white;
        margin-left: 2px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .quickSearchResultDivSelected
    {
        background: #EEEEEE;
        border: 1px solid Gray;
        margin-left: 2px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
</style>
 
<!-- Load the JQuery Libraries that ship with Infuser -->
<script type="text/javascript" src="/_layouts/Muhimbi.Infuser/JQuery/jquery-1.3.2.min.js"></script>
 
<script type="text/javascript">
    // QuickSearch v0.2
    // Created by Jan Tielens, http://weblogs.asp.net/jan
    // Modified to integrate with standard Seachbox by www.muhimbi.com
    // This sample code is provided on an “as is” basis and without warranty of any kind.
 
    // *** 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
        scope: "All Sites",     // search scope to use 'All Sites'
        numberOfResults: 15,    // 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 searchBox = null;
 
    // ** Hook up the various events 
    jQuery.event.add(window, "resize", resizeWindow);
    jQuery.event.add(document, "click", hideResultsDiv);
 
    $(document).ready(function() {
        // ** The searchbox uses a dynamic ID so select it by class     
        searchBox = $('.ms-sbplain');
 
        // Muhimbi, insert the results box after te search area
        searchBox.after("<div id=\"quickSearchResults\" style=\"display: none; z-index:1000\"></div>");
 
        searchBox.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($(searchBox).data("query") != $(searchBox).val()) {
                if (quickSearchTimer != null) // cancel the delayed event
                    clearTimeout(quickSearchTimer);
                quickSearchTimer = setTimeout(function() { // delay the searching
                        $("#quickSearchResults").fadeOut(200, initSearch);
                    } , quickSearchConfig.delay);
            }
        });            
    });
 
    function showResultsDiv(text) {
        var div = $("#quickSearchResults");
        resizeWindow();
        div.append(text).slideDown(quickSearchConfig.resultsAnimation);
    }
 
    function hideResultsDiv() {
        var div = $("#quickSearchResults");
        div.slideUp(quickSearchConfig.resultsAnimation);
        div.empty()
    }
 
    function resizeWindow()
    {
        var div = $("#quickSearchResults");
        var searchParent = $(searchBox).parent();
 
        var divCss = {
            "left": searchParent.offset().left,
            "padding": 0,
            "position": "absolute",
            "top": searchParent.offset().top + searchParent.height() + 1,
            "border": "1px solid #7f9db9",
            "width": searchParent.width(),
            "background": "white",
            "max-width": searchParent.width()
            };
 
        div.css(divCss);
    }
 
    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
        $(searchBox).data("query", $(searchBox).val());
 
        // clear the results
        $("#quickSearchResults").empty();
 
        // start the search
        var query = $(searchBox).val();
        if(query.length >= quickSearchConfig.minCharacters) {
            showResultsDiv("Searching ..."); // display status
            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' >SCOPE:\"" +
                 quickSearchConfig.scope + "\"" + 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: L_Menu_BaseUrl + "/_vti_bin/search.asmx",
            type: "POST",
            dataType: "xml",
            data: soapEnv,
            complete: processResult,
            contentType: "text/xml; charset=\"utf-8\""
        });
 
        function processResult(xData, status) {
            var html = "";
            $(xData.responseXML).find("QueryResult").each(function() {
                var divWidth = $(searchBox).parent().width() + 20;
 
                var x = $("<xml>" + $(this).text() + "</xml>");
                x.find("Document").each(function() {
                    var title = $("Title", $(this)).text();
                    var url = $("Action>LinkUrl", $(this)).text();
                    var description = $("Description", $(this)).text()
 
                    html +=
                        "<div class='quickSearchResultDivUnselected' style='width:" + divWidth +
                              "px;max-width:" + divWidth +"px'> \
                            <a href=\"" + url + "\">" + $("Title", $(this)).text() + "</a> \
                            <div style='display:none' id='details' style='margin-left:10px'>"
                                + description +
                                "<br/>" + url + " \
                            </div> \
                        </div>";
                });
                if(x.find("TotalAvailable").text() != "")
                    html += "<div style='text-align:center; width:" + divWidth +
                            "px'>Total results: " + x.find("TotalAvailable").text() + "</div>";
                else                       
                    html += "<div style='text-align:center; width:" + divWidth +
                            "px'>Total results: 0</div>";
            });
 
            $("#quickSearchResults").empty().append(html);
            $("#quickSearchResults>div>a").hover(
                function() { selectDiv($(this).parent()); },
                function() { unSelectDiv($(this).parent());  }
            );                   
            showResultsDiv();
        }           
    }
 
    function escapeHTML (str) {
       return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }
</script>

.

Labels: , ,

7 Comments:

  • Perfecto! Found just one bug for now: it doesn't recognize (') apostrophes. Returns a page with whatever text is better the apostrophe.

    By Anonymous Anonymous, At 01 October, 2009 19:10  

  • Can this be used in a list-limited or site limited manner rather than searching across "All Sites". I've tried and it hasnt worked.

    By Anonymous Anonymous, At 01 October, 2009 21:44  

  • Sure it can, but it requires a little bit of programming, which is really beyond the scope of this example.

    You will need to update the query that is send to the Search Web Service. A quick Google should give you some examples.

    By Blogger Muhimbi, At 01 October, 2009 22:26  

  • Regarding the issue with the apostrophe, I have never seen one in a SharePoint URL, but I have updated the code.

    Changed line 221 from

    <a href='" + url + "'>" + $("Title", $(this)).text() + "</a> \

    to

    <a href=\"" + url + "\">" + $("Title", $(this)).text() + "</a> \

    By Blogger Muhimbi, At 02 October, 2009 10:59  

  • Any luck with WSS compatibility?

    By Blogger Gee Why, At 25 November, 2009 21:16  

  • Hi Gee,

    This is just a proof of concept, we are not actively developing this script. However, I believe if you look on endusersharepoint.com in the comments then you'll find someone who has made it compatible with WSS.

    Jeroen

    By Blogger Muhimbi, At 26 November, 2009 09:06  

  • Great post. That inspired me to offer another version with a few more enhancements: http://herve20.blogspot.com/2009/12/sharepoint-people-search-autocomplete.html

    By Blogger Herve, At 27 December, 2009 17:25  

Post a Comment

Subscribe to Post Comments [Atom]

Links to this post:

Create a Link