Montag, 24. Februar 2014

Custom Autocomplete for Searchbox using Search Results

Lately I got an request, to give the search Box an "Autocomplete-Feature". OOTB there is already such a Feature, but it works only with fixed and via Central-Administration uploaded Suggestions or the Users personal "favorites" (Queries, that the user did at least 6 times).

The request was, that the SearchBox should Show available words for Searching, matching the users current entry.

CustomSearchBox.AutoCompleteBehavior.prototype = Object.create(AjaxControlToolkit.AutoCompleteBehavior.prototype); CustomSearchBox.AutoCompleteBehavior.prototype._getSuggestion = function () { try { var clientContext = new SP.ClientContext.get_current(); var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(clientContext); So, here is what I did:

The search Box uses the "AjaxControlToolkit.AutoCompleteBehavior", so this is where I started to hook in.

First, I made a custom Class, which basically is a copy of AjaxControlToolkit.AutoCompleteBehavior", and overwrite the method, which is loading the displayed Suggestions: 
Type.registerNamespace('CustomSearchBox');

function initBehavior() {
    CustomSearchBox.AutoCompleteBehavior = function (element) {
        AjaxControlToolkit.AutoCompleteBehavior.initializeBase(this, [element]);
    };

            keywordQuery.set_queryText("*" + this._currentPrefix + "*");
            var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(clientContext);
            var results = searchExecutor.executeQuery(keywordQuery);
            clientContext.executeQueryAsync(Function.createDelegate(this,
                function (sender, e) {
                    try {
                        var rows = results.m_value.ResultTables[0].ResultRows;
                        var reg = new RegExp("\\b\\w*" + this._currentPrefix + "\\w*\\b","ig");
                        var regPart = new RegExp("(" + this._currentPrefix + ")", "ig");
                        var words = [];
                        
                        for (var i = 0; i < rows.length; i++) {
                            var row = rows[i];
                            var hhp = row.HitHighlightedSummary.replace(/<(.|\n)*?>/g, '');

                            var match = reg.exec(hhp);
                            if (match) {
                                for (var j = 0; j < match.length; j++) {
                                    {
                                        var bolded = match[j].replace(regPart, "$1");
                                        if (words.indexOf(bolded) == -1)
                                            words.push(bolded);
                                    }
                                }
                            }
                        }

                        var completionItems = new Microsoft.SharePoint.Client.Search.Query.QuerySuggestionResults();

                        completionItems.set_queries(words);
                        completionItems.set_peopleNames([]);
                        completionItems.set_personalResults([]);

                        this._update(this._currentPrefix, completionItems, true);
                    }
                    catch (ex) {
                        Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._update", ex.toString());
                    }
                }),
                function (sender, failureArgs) { });
        }
        catch (e) {
            Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._getSuggestion", e.toString());
        }
        $common.updateFormToRefreshATDeviceBuffer();
    };
    
    CustomSearchBox.AutoCompleteBehavior.registerClass('CustomSearchBox.AutoCompleteBehavior', Sys.UI.Behavior);
    CustomSearchBox.AutoCompleteBehavior.descriptor = AjaxControlToolkit.AutoCompleteBehavior.descriptor;
}

function activateCustomSuggestionBehavior(control) {
    if (control.$3G_3) {
        SP.SOD.executeFunc('sp.js');
        EnsureScriptFunc('ajaxtoolkit.js', 'AjaxControlToolkit.AutoCompleteBehavior', function() {
            if (Srch.U.n(control.$f_3[control.$I_3])) {
                initBehavior();
                var $v_0 = {};
                $v_0['id'] = control.$I_3;
                $v_0['completionInterval'] = control.$2y_3;
                $v_0['completionListElementID'] = control.$I_3;
                $v_0['parentElementID'] = control.$c_3;
                $v_0['completionListCssClass'] = 'ms-qSuggest-list';
                $v_0['completionListItemCssClass'] = 'ms-qSuggest-listItem';
                $v_0['personalResultTitleCssClass'] = 'ms-qSuggest-personalResultTitle';
                $v_0['hrCSSClass'] = 'ms-qSuggest-listSeparator';
                $v_0['enterKeyDownScript'] = '$find(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.get_id()) + '\').search($get(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.$d_3) + '\').value)';
                $v_0['highlightedItemCssClass'] = 'ms-qSuggest-hListItem';
                $v_0['minimumPrefixLength'] = control.$30_3;
                $v_0['offsetWidth'] = -2;
                $v_0['queryCount'] = control.$2z_3;
                $v_0['personalResultCount'] = control.$2w_3;
                $v_0['showPeopleNameSuggestions'] = control.$3F_3;
                $v_0['personalResultTitleSingular'] = Srch.Res.qs_PersonalResultTitleSingular;
                $v_0['personalResultTitlePlural'] = Srch.Res.qs_PersonalResultTitlePlural;
                $v_0['overlappingElementID'] = control.$F_3;
                $v_0['sourceId'] = control.get_querySuggestionsSourceID();
                Srch.U.createBehavior(control.$I_3, CustomSearchBox.AutoCompleteBehavior, $v_0, control.$d_3);
                control.$f_3[control.$I_3] = true;
            }
        });
    }
}
Then I created a custom DisplayTemplate which includes the scriptfile with the above js-code:

    <script>

        $includeScript(this.url, "~sitecollection/Style Library/Custom/JSLibraries/Custom.Search.js");

    </script>


and change the onkeydown function of the corresponding input

  <input type="text" value="_#= $htmlEncode(ctx.ClientControl.get_currentTerm()) =#_" maxlength="2048"

    accesskey="_#= $htmlEncode(Srch.Res.sb_AccessKey) =#_"

    title="_#= $htmlEncode(prompt) =#_"

    id="_#= $htmlEncode(searchBoxId) =#_" autocomplete="off" autocorrect="off"

    onkeypress="if (Srch.U.isEnterKey(String.fromCharCode(event.keyCode))) { $getClientControl(this).search(this.value);return Srch.U.cancelEvent(event); }"

    onkeydown="var ctl = $getClientControl(this); activateCustomSuggestionBehavior(ctl);"

    onfocus="var ctl = $getClientControl(this);ctl.hidePrompt();ctl.setBorder(true);"

    onblur="var ctl = $getClientControl(this);ctl.showPrompt();ctl.setBorder(false);"

    class="_#= inputClass =#_" />


Last but not least I had to assign the new Displaytemplate to the SearchBox, and I had a Autocomplete-Suggestion function using the search.

Anyway, as the sharepoint search is used, you have to keep following in mind:
- Search delivers and orders results based on the current user, so it is possible that the autocomplete delivers different results for different users
- Search delivers results in pages (default 10 items per page), so in the above version, if all of the items on the first page contains the same word, you might get only one word in the autocomplete box. You can change this by changing the pagesize in the query issued by the autocomplete-suggestion function. But keep in mind: the more items you load, the greater is the impact on performance
- If it doesn´t work check the following:
 -- necessary SP-js-files are loaded
 -- ShowPeopleSuggestions is activated in the search-box Webparts properties

Of course you could also do that by putting the whole JS-Code in a Content-Editor-Webpart and overwrite the onkeydown-function there. However, in my opinion the above is the cleanest and most modular solution.

Regards