﻿/// <creation>
/// 2009-07-07  EF      Suggestions client.
/// 2009-11-05  EF      If in "Combo" mode and only one possible suggestion ==> set this one
/// 2009-11-19  EF      Add "limitTextSize" property.
/// 2009-11-20  EF      Process events (change) when text length is zero.
///                     Ignore tab key (fired when entering the field).
/// 2009-12-10  EF      Fix BUG: call saved events in combo mode when there's no selected entity (key control).
/// 2009-11-05  EF      If in "Combo" mode and there are suggestions ==> set the first one
/// </creation>

//  Define suggestions handler class

//  Default parameters (global class variable)
SuggestionsHandler.defaultParameters = new Object();
SuggestionsHandler.traceMessageNumber = 0;

//  Parameters:
//  - inputControl: control where text is typed
//  - parameters: object containing following parameters
//      . 
function SuggestionsHandler(inputControl, parameters) {
    if (!parameters) {
        parameters = new Object();
    }

    var _messagePrefix = "jQuery.Suggestions plug-in: ";

    this.BuildMessage = function(text) {
        return _messagePrefix + text;
    }

    var _debug = parameters.debug || SuggestionsHandler.defaultParameters.debug || false;
    var _debugWindowId = parameters.debugWindowId || SuggestionsHandler.defaultParameters.debugWindowId || "_suggestionsDebugWindow";

    this.TraceMessage = function(message) {
        //        alert('traceMessage ' + message + ', debug: ' + _debug + ', controlId: ' + inputControl.id);
        SuggestionsHandler.traceMessageNumber++;
        var completeMessage = "Suggest.DEBUG#" + SuggestionsHandler.traceMessageNumber + " " + message;
        if (_debug) {
            jQuery("#" + _debugWindowId).html(completeMessage + "<br/>" + jQuery("#" + _debugWindowId).html());
        }
    }

    //====  Cache and timeouts
    //  Suggestions local cache
    var _cache = new Object();

    //  Timout to show suggestions after a key is presses 
    //  TODO: implement timeout adjustement strategy depending on suggestions request (e.g. web service) response time
    var _showSuggestionsTimeout = parameters.showSuggestionsTimeout || SuggestionsHandler.defaultParameters.showSuggestionsTimeout;
    var _optimizeRequests = parameters.optimizeRequests || SuggestionsHandler.defaultParameters.optimizeRequests || false;
    var _keyupLastTimeoutId = null; //  timeout id for canceling if necessary

    //  Timeout to hide suggestions when control looses focus
    var _lostFocusTimeout = parameters.lostFocusTimeout || SuggestionsHandler.defaultParameters.lostFocusTimeout;

    //====  Events
    var _previousDomOnChangeEvent = null;
    var _previousJQueryChangeEvents = null;

    //====  Suggestions
    //  Suggestion handler type
    //  - "classic": google like suggestions.
    //  - "combo": drop down list with suggestions.
    var _suggestionBehavior = parameters.behavior || SuggestionsHandler.defaultParameters.behavior;
    if (_suggestionBehavior != "classic" && _suggestionBehavior != "combo") {
        throw this.BuildMessage("Incorrect suggestion behavior \"" + _suggestionBehavior + "\", valid values are \"classic\" and \"combo\"");
    }

    var _limitTextSize = true;
    if (typeof (parameters.limitTextSize) != "undefined") {
        _limitTextSize = parameters.limitTextSize;
    }
    else if (typeof (SuggestionsHandler.defaultParameters.limitTextSize) != "undefined") {
        _limitTextSize = SuggestionsHandler.defaultParameters.limitTextSize;
    }

    //  Field where the suggestions collection is returned from WS or whatever, if no field
    //  is specified, the "return" object intended to be the collection.
    //  E.g. in ASP.Net WebMethods is "d" (in callback suggestions = result.d).
    var _suggestionField = null;
    if (typeof (parameters.suggestionField) != "undefined") {
        //  Particular case, "null" is a valid value, same for ""
        _suggestionField = parameters.suggestionField;
    }
    else {
        _suggestionField = parameters.suggestionField || SuggestionsHandler.defaultParameters.suggestionField;
    }
    //  Text field in suggestion object
    var _suggestionTextField = parameters.suggestionTextField || SuggestionsHandler.defaultParameters.suggestionTextField;
    //  Suggestion type field
    //  0. text suggestion.
    //  1. entity suggestion (with a key).
    //  2. action suggestion (TODO: define behavior)
    var SUGGESTION_TYPE_TEXT = 2;
    var SUGGESTION_TYPE_ENITTY = 2;
    var SUGGESTION_TYPE_ACTION = 2;
    var _suggestionTypeField = parameters.suggestionTypeField || SuggestionsHandler.defaultParameters.suggestionTypeField;
    //  Key field in suggestion object
    //  If suggestion type es "entity", this is the entity key
    var _suggestionKeyField = parameters.suggestionKeyField || SuggestionsHandler.defaultParameters.suggestionKeyField;

    var _suggestionUrlField = parameters.suggestionUrlField || SuggestionsHandler.defaultParameters.suggestionUrlField;

    //  Current calculated suggestions
    var _currentSuggestions = new Array();

    //  Currently selected suggestion
    var _selectedSuggestion = -1;

    //====  Content: input control and variables
    //  Input control (e.g. textbox)
    var _inputControl = inputControl;

    //  Control to hold the "key" when in combo mode
    var _keyControl = null;

    if (parameters.keyControl) {
        _keyControl = parameters.keyControl;
    }
    else if (parameters.keyControlId) {
        _keyControl = document.getElementById(parameters.keyControlId);
    }

    if (_suggestionBehavior == "combo" && !_keyControl) {
        //  An input field to hold the selected key must be provided
        _keyControl = document.createElement("input");
        _keyControl.id = "_suggestionKey_" + _inputControl.id;
        //_keyControl.type = "text";
        _keyControl.type = "hidden";
        _keyControl.value = "";

        //  Insert hidden control after input control
        jQuery(_inputControl).after(_keyControl);
    }

    var _minimumInputLength = parameters.minimumInputLength || SuggestionsHandler.defaultParameters.minimumInputLength || 1;
    var _maximumResults = parameters.maximumResults || SuggestionsHandler.defaultParameters.maximumResults || -1;
    this.TraceMessage("Parameters: maximumResults: " + _maximumResults);
    //  Previous text value before suggestions where calculated
    var _previoustText = "";
    var _previoustKey = "";

    //  Current value of input control
    var _currentText = jQuery(_inputControl).val();
    var _currentKey = jQuery(_keyControl).val();

    //====  Request parameters
    var _requestUrl = parameters.requestUrl || SuggestionsHandler.defaultParameters.requestUrl;
    var _requestType = parameters.requestType || SuggestionsHandler.defaultParameters.requestType;
    var _requestContentType = parameters.requestContentType || SuggestionsHandler.defaultParameters.requestContentType;
    var _requestDataTextField = parameters.requestDataTextField || SuggestionsHandler.defaultParameters.requestDataTextField;
    var _requestDataMaximumResultsField = parameters.requestDataMaximumResultsField || SuggestionsHandler.defaultParameters.requestDataMaximumResultsField;
    var _requestDataExtraFields = parameters.requestDataExtraFields || SuggestionsHandler.defaultParameters.requestDataExtraFields || ""
    var _requestDataType = parameters.requestDataType || SuggestionsHandler.defaultParameters.requestDataType;
    var _requestJsonpCallbackParameter = parameters.requestJsonpCallbackParameter || SuggestionsHandler.defaultParameters.requestJsonpCallbackParameter;

    //  Counts request: for debugging purposes only
    var _requestCounter = 0;

    //====  UI, styles and style parameters
    //  UI container for suggestions
    var _uiContainerType = "div";
    var _uiContainerName = "_suggestionsContainer";

    //  Tag name for the "style" tag with dynamic styles
    var _cssStyleTagId = "suggestionsCssStyle_" + _inputControl.id;

    //  Classes for container (div)
    var _mustAddCssClassContainer = !parameters.cssClassContainer && !SuggestionsHandler.defaultParameters.cssClassContainer;
    var _cssClassContainer = parameters.cssClassContainer || SuggestionsHandler.defaultParameters.cssClassContainer
        || ("suggestionsContainer_" + _inputControl.id);
    var _cssStyleContainer = parameters.cssStyleContainer || SuggestionsHandler.defaultParameters.cssStyleContainer;

    //  Classes for selected and unselected items on container
    var _mustAddCssClassSelectedItem = !parameters.cssClassSelectedItem && !SuggestionsHandler.defaultParameters.cssClassSelectedItem;
    var _mustAddCssClassUnSelectedItem = !parameters.cssClassUnSelectedItem && !SuggestionsHandler.defaultParameters.cssClassUnSelectedItem;
    var _cssClassSelectedItem = parameters.cssClassSelectedItem || SuggestionsHandler.defaultParameters.cssClassSelectedItem
        || ("suggestionSelected_" + _inputControl.id);
    var _cssClassUnSelectedItem = parameters.cssClassUnSelectedItem || SuggestionsHandler.defaultParameters.cssClassUnSelectedItem
        || ("suggestionUnSelected_" + _inputControl.id);

    var _cssStyleSelectedItem = parameters.cssStyleSelectedItem || SuggestionsHandler.defaultParameters.cssStyleSelectedItem;
    var _cssStyleUnSelectedItem = parameters.cssStyleUnSelectedItem || SuggestionsHandler.defaultParameters.cssStyleUnSelectedItem;

    //  Suggestions container position and width
    var _containerTopOffset = parameters.containerTopOffset || SuggestionsHandler.defaultParameters.containerTopOffset || 0;
    var _containerLeftOffset = parameters.containerLeftOffset || SuggestionsHandler.defaultParameters.containerLeftOffset || 0;
    var _containerWidthOffset = parameters.containerWidthOffset || SuggestionsHandler.defaultParameters.containerWidthOffset || 0;

    //  Custom handlers
    var _onLoad = parameters.onLoad || SuggestionsHandler.defaultParameters.onLoad;

    //====  Semaphores
    //  Semaphore to prevent listener reentering code
    var _doingRequest = false;

    //  Semaphore to prevent hiding when showing
    var _showing = false;

    //====  Protected methods

    this.GetContainer = function() {
        return jQuery("#" + _uiContainerName);
    }

    this.GetInputControl = function() {
        return jQuery(_inputControl);
    }

    this.GetKeyControl = function() {
        return jQuery(_keyControl);
    }

    //            this.CreateKeyControl = function() {
    //                var keyControl = document.createElement("input");
    //                keyControl.id = "_suggestionKey_" + _inputControl.id;
    //                keyControl.type = "hidden";
    //                keyControl.value = "";
    //                return keyControl;
    //            }

    this.ShowContainer = function() {
        var thisObject = this;
        //  Position container
        this.PositionControlAfter(this.GetInputControl(), this.GetContainer());

        //        var inputWidth = _inputControl.style.width;

        if (_currentSuggestions.length > 0) {
            //  Show container
            this.GetContainer().show();
        }
        else {
            this.GetContainer().hide();
        }

    }

    this.HideContainer = function() {
        this.TraceMessage("Hide Container");
        return this.GetContainer().hide();
    }

    this.BuildCacheKey = function() {
        //return _inputControl.id + "|" + _currentText;
        return _currentText;
    }

    // reposition the results div accordingly to the search field
    this.PositionControlAfter = function(referenceControl, controlToPosition) {
        var referencePosition = referenceControl.offset();

        //  Set new position by CSS
        controlToPosition.css({
            position: "absolute",
            left: referencePosition.left + _containerLeftOffset,
            top: referencePosition.top + referenceControl.height() + _containerTopOffset,
            width: referenceControl.width() + _containerWidthOffset
        });

        //	        controlToPosition.css("position", "absolute");
        //	        controlToPosition.css("left", referencePosition.left);
        //	        controlToPosition.css("top", referencePosition.top + referenceControl.height() + 3);
        //	        controlToPosition.css("width", referenceControl.width() + 1);
    }

    //  Apply styles depending on received parameters
    this.AddStyles = function() {
        if (_mustAddCssClassSelectedItem || _mustAddCssClassUnSelectedItem || _mustAddCssClassContainer) {
            var styleTagHtml = "<style id=\"" + _cssStyleTagId + "\" type=\"text/css\">";

            if (_mustAddCssClassSelectedItem) {
                styleTagHtml += "." + _cssClassSelectedItem + " {" + _cssStyleSelectedItem + "}";
            }
            if (_mustAddCssClassUnSelectedItem) {
                styleTagHtml += "." + _cssClassUnSelectedItem + " {" + _cssStyleUnSelectedItem + "}";
            }
            if (_mustAddCssClassContainer) {
                styleTagHtml += "." + _cssClassContainer + " {" + _cssStyleContainer + "}";
            }

            styleTagHtml += "</style>";

            jQuery("head").append(styleTagHtml);
        }
    }

    this.SaveAndUnbindEvents = function() {
        this.TraceMessage("SaveAndUnbindEvents() " + typeof (_inputControl.onchange) + " | " + _inputControl.onchange);


        if (_inputControl.onchange) {
            _previousDomOnChangeEvent = _inputControl.onchange;
            _inputControl.onchange = null;
            this.TraceMessage("_previousDomOnChangeEvent :" + _previousDomOnChangeEvent);
        }

        if (jQuery.data(_inputControl, "events") && jQuery.data(_inputControl, "events").change) {
            //  Must explicitly save events code one by one because after "unbind", jQuery.data(_inputControl, "events").change does not has any content
            _previousJQueryChangeEvents = new Object();

            jQuery.each(jQuery.data(_inputControl, "events").change, function(eventKey, eventCode) {
                _previousJQueryChangeEvents[eventKey] = eventCode;
            });

            this.TraceMessage("_previousJQueryChangeEvents :" + _previousJQueryChangeEvents);
        }

        jQuery(_inputControl).unbind("change");

    }

    this.ProcessSavedEvents = function() {
        this.TraceMessage("ProcessSavedEvents()");
        if (_previousDomOnChangeEvent) {
            this.TraceMessage("Process _previousDomOnChangeEvent: " + _previousDomOnChangeEvent);

            _previousDomOnChangeEvent();
        }

        if (_previousJQueryChangeEvents) {
            for (var eventKey in _previousJQueryChangeEvents) {
                //eval("var __myFunction = " + _previousJQueryChangeEvents[eventKey] + "; __myFunction();");
                _previousJQueryChangeEvents[eventKey]();
            }
        }

        //        var inputControl = document.getElementById(_inputControl.id);
        //        //if (jQuery.data(_inputControl, "events") && jQuery.data(_inputControl, "events").change) {
        //        jQuery.each(jQuery.data(inputControl, "events").change, function(key, value) {
        //            alert("Process: " + value);
        //            eval(value);
        //        });
        //        //}
    }

    this.RestoreSavedEvents = function() {
        this.GetInputControl().unbind('keyup', this.KeyupEvent);
        this.GetInputControl().unbind('blur', this.BlurEvent);

        if (_previousDomOnChangeEvent) {
            _inputControl.onchange = _previousDomOnChangeEvent;
        }

        if (_previousJQueryChangeEvents) {
            for (var eventKey in _previousJQueryChangeEvents) {
                this.GetInputControl().change(_previousJQueryChangeEvents[eventKey]);
            }
        }
    }

    this.RemoveStyles = function() {
        if (_mustAddCssClassSelectedItem || _mustAddCssClassUnSelectedItem || _mustAddCssClassContainer) {
            jQuery("head").remove("#" + _cssStyleTagId);
        }
    }

    this.ApplySelectedSuggestion = function(selectedSuggestion) {
        var thisObject = this;

        this.GetContainer().children().each(function(index) {
            if (index == selectedSuggestion) {
                if (jQuery(this).find("a").length == 0) {
                    //  Not a link (action)
                    thisObject.GetInputControl().val(jQuery(this).children(":input").eq(0).val());
                    thisObject.GetKeyControl().val(jQuery(this).children(":input").eq(2).val());
                    thisObject.ProcessSavedEvents();

                }
                else {
                    //  It's not a text suggstion
                    //  Revert to previous values
                    thisObject.GetInputControl().val(_previoustText);
                    thisObject.GetKeyControl().val(_previoustKey);
                }

                this.className = _cssClassSelectedItem;
            }
            else {
                this.className = _cssClassUnSelectedItem;
            }
        });
    }

    //  Handle arrow keys (up and down)
    this.HandleArrowKeys = function(keyCode) {
        //  To use inside jQuery expressions due to "this" ambiguity
        var thisObject = this;

        if (keyCode == 40 || keyCode == 38) {
            //  Navigate through suggestions
            if (keyCode == 38) {
                //  Up arrow
                if (_selectedSuggestion < 0) {
                    //  Is in textbox and presses up arrow ==> go to last suggestion
                    _selectedSuggestion = this.GetContainer().children().length - 1;
                }
                else if (_selectedSuggestion == 0) {
                    //                        _selectedSuggestion = this.GetContainer().children().length - 1;
                    _selectedSuggestion = -1;
                }
                else {
                    _selectedSuggestion--;
                }
            }
            else {
                //  Down arrow
                if (_selectedSuggestion == this.GetContainer().children().length - 1) {
                    //_selectedSuggestion = 0;
                    _selectedSuggestion = -1;
                }
                else {
                    _selectedSuggestion++;
                }
            }

            // loop through each result div applying the correct style
            this.ApplySelectedSuggestion(_selectedSuggestion);

            if (_selectedSuggestion < 0) {
                //  Leaving suggestions list: up arrow on first element or down arrow on last element.
                //  Go back to original content
                thisObject.GetInputControl().val(_previoustText);
                thisObject.GetKeyControl().val(_previoustKey);

                _currentText = _previoustText;
                _currentKey = _previoustKey;
                //jQuery("#newKey").val("1: " + _currentKey);
            }

            return true;
        }
        else {
            //  Clear selection
            //            _selectedSuggestion = -1;

            return false;
        }
    }

    this.BuildSuggestionHtmlItem = function(suggestionText, inputText) {
        var suggestionHtml = null;
        if (suggestionText.length > inputText.length) {
            //  Normal case: suggestion text is larger than input text
            if (_suggestionBehavior == "classic") {
                //  Format suggestion for "classic". It remainder selection characters (like google)
                var textPart1 = suggestionText.substring(0, inputText.length);
                var textPart2 = suggestionText.substring(textPart1.length);

                suggestionHtml = textPart1 + "<strong>" + textPart2 + "</strong>";
            }
            else if (_suggestionBehavior == "combo") {
                //  Format suggestion for "combo". It highlights whenever input text apperas in suggestion
                var lowerText = suggestionText.toLowerCase();
                //var lowerInput = this.GetInputControl().val().toLowerCase();
                var lowerInput = inputText.toLowerCase();

                var textParts = lowerText.split(lowerInput);

                suggestionHtml = "";
                var l = 0;
                for (var j = 0; j < (textParts.length - 1); j++) {
                    suggestionHtml += suggestionText.substring(l, l + textParts[j].length) + "<strong>" +
                            suggestionText.substring(l + textParts[j].length, l + textParts[j].length + lowerInput.length) + "</strong>";
                    l += textParts[j].length + lowerInput.length;

                }

                suggestionHtml += suggestionText.substring(l, l + textParts[j].length);

            }

        }
        else {
            //  Suggestion is the same as input text
            if (_suggestionBehavior == "classic") {
                suggestionHtml = suggestionText;
            }
            else {
                suggestionHtml = "<strong>" + suggestionText + "</strong>";
            }
        }

        return suggestionHtml;
    }

    this.CalculateVisualWidth = function(element) {
        var ruler = document.createElement("span");

        if (typeof (element) === "string" && element.substring(0, 0) != "#") {
            //  Pure html string
            ruler.innerHTML = element;
        }
        else {
            //  DOM element or jQuery #id selector
            ruler.innerHTML = jQuery(element).html();
        }
        jQuery(ruler).css = { "visibility": "hidden" };
        jQuery("body").append(ruler);
        var returnValue = jQuery(ruler).width();
        //var returnValue = jQuery(ruler).get(0).offsetWidth;
        jQuery(ruler).remove();

        return returnValue;
    }

    this.TransformActionUrl = function(url) {
        return url;
    }

    this.CloneSuggestions = function(suggestions) {
        var clonedSuggestions = new Array();

        for (var i = 0; i < suggestions.length; i++) {
            //  jQuery way to clone an object (deep copy)
            var clonedSuggestion = jQuery.extend({}, suggestions[i]);

            clonedSuggestions[i] = clonedSuggestion;
        }

        return clonedSuggestions;
    }

    this.PreProcessSuggestions = function(suggestions) {
        if (_onLoad) {
            var alteredSuggestions = this.CloneSuggestions(suggestions);

            _onLoad(this, alteredSuggestions);

            return alteredSuggestions;
        }
        else {
            return suggestions;
        }
    }

    //  Classic google like suggestions
    this.ShowSuggestionsClassic = function(suggestions) {
        //if (this.GetInputControl().val().length == 0) {
        if (this.GetInputControl().val().length < _minimumInputLength) {
            return;
        }

        //  To use inside jQuery expressions due to "this" ambiguity
        var thisObject = this;

        _showing = true;

        //  Recreate suggestion's UI container (e.g. DIV)
        this.GetContainer().empty();

        //  Add suggestions text to UI container
        for (var index = 0; index < suggestions.length; index++) {
            //  Create inner DIV
            var innerDiv = document.createElement("div");

            //  Set css class to "unselected", add suggestion text and append to container
            innerDiv.className = _cssClassUnSelectedItem;
            var jInnerDiv = jQuery(innerDiv);

            var inputTextLength = this.GetInputControl().val().length;
            var text = suggestions[index][_suggestionTextField];
            var type = _suggestionTypeField ? suggestions[index][_suggestionTypeField] : SUGGESTION_TYPE_TEXT;
            var key = _suggestionKeyField ? suggestions[index][_suggestionKeyField] : null;
            var url = _suggestionUrlField ? suggestions[index][_suggestionUrlField] : null;
            var limitTextSize = _limitTextSize;

            var fits = false;
            var loopCounter = 0;

            //  Build sugestion HTML item and make it fit in input control width
            while (!fits) {
                var suggestionHtml = null;
                var visualWidth = null;
                if (type == SUGGESTION_TYPE_ACTION) {
                    suggestionHtml = text;
                }
                else {
                    suggestionHtml = this.BuildSuggestionHtmlItem(text, this.GetInputControl().val()); // _currentText
                }

                jInnerDiv.html(suggestionHtml);

                //var visualWidth = this.CalculateVisualWidth(jInnerDiv.html());
                var visualWidth = this.CalculateVisualWidth(jInnerDiv);

                if (limitTextSize && type != SUGGESTION_TYPE_ACTION && visualWidth > this.GetInputControl().width() && text.length > 5) {
                    //  Suggestion DIV is larger than input control width (maximum container allowed width)
                    //  Must shorten the string
                    this.TraceMessage("LARGER: index: " + index + ", text.length: " + text.length +
                        ", ruler.width(): " + visualWidth + ", this.GetInputControl().width(): " + this.GetInputControl().width() + ", suggestionHtml: " + suggestionHtml);

                    //  Remove last 4 character and replace with "..."
                    var charsToRemove = loopCounter == 0 ? 1 : 4;
                    text = text.substring(0, text.length - charsToRemove) + "...";
                }
                else {

                    if (type == SUGGESTION_TYPE_ACTION && url) {
                        var transformedUrl = this.TransformActionUrl(url);

                        //  Action pointing to an URL
                        jInnerDiv.html("<a href=\"" + transformedUrl + "\" title=\"" + suggestions[index][_suggestionTextField] + "\">" + suggestionHtml + "</a>");
                    }
                    else {
                        //  Text
                        jInnerDiv.html("<span title=\"" + suggestions[index][_suggestionTextField] + "\">" + suggestionHtml + "</span>");
                    }

                    jInnerDiv.append("<input type='hidden' value ='" + text + "'/>").
                    append("<input type='hidden' value ='" + index.toString() + "'/>");

                    if (key) {
                        jInnerDiv.append("<input type='hidden' value ='" + key + "'/>");
                    }

                    jInnerDiv.appendTo("#" + _uiContainerName);
                    fits = true;
                }

                loopCounter++;
            }


        }

        //  Select inner divs
        var innerDivs = jQuery("#" + _uiContainerName + " > div");

        //  On mouse over highlight current suggestion (innerDiv)
        innerDivs.mouseover(function() {
            innerDivs.each(function() {
                this.className = (_cssClassUnSelectedItem);
            });

            this.className = (_cssClassSelectedItem);

            //  Second hidden input contains the index
            _selectedSuggestion = parseInt(jQuery(this).children(":input").eq(1).val());
        });

        //  On click select current suggestion
        innerDivs.click(function() {
            thisObject.TraceMessage("innerDivs.click");

            //  First hidden input contains the whole suggestion text
            var jThis = jQuery(this);

            var anchor = jThis.find("a").get(0);
            if (anchor) {
                //  Action link
                window.location = anchor.href;
            }
            else {
                //  Text suggestion selection
                thisObject.TraceMessage("DIV click");

                _selectedSuggestion = parseInt(jThis.children(":input").eq(1).val());

                thisObject.ApplySelectedSuggestion(_selectedSuggestion);
                //                thisObject.GetInputControl().val(jThis.children(":input").eq(0).val());
                //                thisObject.GetKeyControl().val(jThis.children(":input").eq(2).val());

                //                thisObject.ProcessSavedEvents();
            }

            thisObject.HideContainer();
        });

        //  Show container
        this.ShowContainer();

        _showing = false;
    };

    //  Get suggestions and show them
    this.GetAndShowSuggestions = function() {
        this.TraceMessage("GetAndShowSuggestions(): _requestCounter: " + _requestCounter +
            ", _keyupLastTimeoutId: " + _keyupLastTimeoutId + ", _doingRequest: " + _doingRequest);
        //if (_currentText.length == 0) {
        if (_currentText.length < _minimumInputLength) {
            _currentKey = "";
            //jQuery("#newKey").val("2: " + _currentKey);
            this.TraceMessage("GetAndShowSuggestions(): Hide");
            this.HideContainer();
        }

        if (_previoustText != _currentText && !_doingRequest) {
            _previoustText = _currentText;
            _previoustKey = _currentKey;
            //jQuery("#oldKey").val("1: " + _previoustKey);

            //  Switch semaphore on
            _doingRequest = true;

            _selectedSuggestion = -1;

            //  First look in cache
            var cacheKey = this.BuildCacheKey();

            var cachedContent = _cache[cacheKey];

            if (cachedContent) {
                //  Found in cache
                _currentSuggestions = cachedContent;
                //this.TraceMessage('GetAndShowSuggestions(): cache s[0] ' + _currentSuggestions && _currentSuggestions.length > 0 ? _currentSuggestions[0].Text : "cache sinSuge??");

                this.PostGetAndShowSuggestions();
            }
            else {

                //  Call service to get new suggestions, sets _suggestions on callback
                this.GetSuggestionsFromService();
            }


            //            this.ShowSuggestions(_currentSuggestions);

            //            //  When in "combo" mode, check if text is exactly one of the suggesions. 
            //            //  If it is so, assign key.
            //            if (_suggestionBehavior == "combo" && _suggestionKeyField) {
            //                var found = false;
            //                for (var i = 0; i < _currentSuggestions.length; i++) {
            //                    if (_currentSuggestions[i][_suggestionKeyField] && _currentSuggestions[i][_suggestionTextField] == _currentText) {
            //                        _currentKey = _currentSuggestions[i][_suggestionKeyField];
            //                        this.GetKeyControl().val(_currentKey);
            //                        //jQuery("#newKey").val("3: " + _currentKey);
            //                        found = true;

            //                        break;
            //                    }
            //                }

            //                if (!found) {
            //                    this.GetKeyControl().val("");
            //                }
            //            }

            //  Set focus to suggestion control
            _inputControl.focus();

            //  Switch semaphore off: done in ajax callback function
            //_doingRequest = false;
        }
    };

    this.PostGetAndShowSuggestions = function() {
        this.ShowSuggestions(_currentSuggestions);

        //  When in "combo" mode, check if text is exactly one of the suggesions. 
        //  If it is so, assign key.
        if (_suggestionBehavior == "combo" && _suggestionKeyField) {
            var found = false;
            for (var i = 0; i < _currentSuggestions.length; i++) {
                if (_currentSuggestions[i][_suggestionKeyField] && _currentSuggestions[i][_suggestionTextField] == _currentText) {
                    _currentKey = _currentSuggestions[i][_suggestionKeyField];
                    this.GetKeyControl().val(_currentKey);
                    //jQuery("#newKey").val("3: " + _currentKey);
                    found = true;

                    break;
                }
            }

            if (!found) {
                this.TraceMessage("PostGetAndShowSuggestions(). not found. Set Key control to ''");
                this.GetKeyControl().val("");
                this.ProcessSavedEvents();
            }
        }

        //  Switch semaphore off
        _doingRequest = false;
    }

    var thisObject = this;
    this.KeyupEvent = function(event) {

        //  Get key code (depending on browser)
        var keyCode = event.keyCode || event.charCode || window.event.keyCode;

        if (keyCode == 9) {
            //  Tab: Entering control
            return;
        }

        var inputControl = thisObject.GetInputControl();
        var keyControl = thisObject.GetKeyControl();

        _currentText = inputControl.val();
        _currentKey = keyControl.val();

        //  Handle up/down arrow keys
        if (thisObject.HandleArrowKeys(keyCode)) {
            return;
        }

        thisObject.TraceMessage("KeyupEvent(): thisObject.GetInputControl().val().length = " +
            thisObject.GetInputControl().val().length);

        //  Handle special keys (enter, esc)
        if (keyCode == 13 || keyCode == 27 || thisObject.GetInputControl().val().length == 0) {
            if (keyCode == 27) {
                //  Esc: key, revert to previous value
                inputControl.val(_previoustText);
                keyControl.val(_previoustKey);

                _currentText = _previoustText;
                _currentKey = _previoustKey;

                _selectedSuggestion = -1;
                //jQuery("#newKey").val("5: " + _currentKey);
            }
            else if (keyCode == 13) {
                var anchor = thisObject.GetContainer().find("a").get(_selectedSuggestion);
                thisObject.TraceMessage("_selectedSuggestion: " + _selectedSuggestion + ", anchor: " + anchor);
                if (anchor) {
                    //  Action link
                    window.location = anchor.href;
                }
            }

            if (thisObject.GetInputControl().val().length == 0) {
                thisObject.TraceMessage("KeyupEvent(). input control length = 0. set key control to ''");
                keyControl.val(_previoustKey).val("");
                thisObject.ProcessSavedEvents();
            }

            thisObject.HideContainer();
            return;
        }

        //  Next run
        //        thisObject.TraceMessage("KeyupEvent() NextRun: _requestCounter: " + _requestCounter + ", _keyupLastTimeoutId: " + _keyupLastTimeoutId +
        //            ", _optimizeRequests: " + _optimizeRequests + ", _currentText: " + _currentText);
        if (_optimizeRequests && _keyupLastTimeoutId != null && thisObject.GetInputControl().val().length > 0) {
            //  Cancel previous timeout in order to prevent incouous calls
            //thisObject.TraceMessage("KeyupEvent() ClearTimeout: _requestCounter: " + _requestCounter + ", _keyupLastTimeoutId: " + _keyupLastTimeoutId);
            clearTimeout(_keyupLastTimeoutId);
        }

        _keyupLastTimeoutId = setTimeout(function() { thisObject.GetAndShowSuggestions() }, _showSuggestionsTimeout);
    }

    this.GetSuggestionsCallback = function(result) {
        if (_suggestionField && _suggestionField.length > 0) {
            _currentSuggestions = result[_suggestionField];
        }
        else {
            _currentSuggestions = result;
        }

        _cache[this.BuildCacheKey()] = _currentSuggestions;
        //this.TraceMessage('GetSuggestionsCallback(): srv s[0] ' + _currentSuggestions && _currentSuggestions.length > 0 ? _currentSuggestions[0].Text : "srv sinSuge??");

        this.PostGetAndShowSuggestions();
    }

    this.CreateContainer = function() {
        var container = document.createElement(_uiContainerType);
        container.id = _uiContainerName;
        container.className = _cssClassContainer;

        return container;
    }

    //  Get suggestions
    this.GetSuggestionsFromService = function() {

        //  To use inside jQuery expressions due to "this" ambiguity
        var thisObject = this;

        var suggestions = null;

        var dataParameter = _requestDataTextField + ": '" + thisObject.GetInputControl().val().replace("'", "\'") + "'";

        if (_requestDataMaximumResultsField) {
            dataParameter += ", " + _requestDataMaximumResultsField + ": " + _maximumResults.toString();
        }
        if (_requestDataExtraFields) {
            dataParameter += ", " + _requestDataExtraFields;
        }

        //this.TraceMessage("GetSuggestionsFromService(): dataParameter: " + dataParameter);

        var ajaxParameters = {
            async: true,
            type: _requestType,
            contentType: _requestContentType,
            url: _requestUrl,
            dataType: _requestDataType,
            //                success: thisObject.GetSuggestionsCallback,
            success: jQuery.callWithinScope(thisObject, "GetSuggestionsCallback"),
            error: function(httpClient, errorStatus, errorText) {
                throw "jQuery.Suggestions plug-in: GetSuggestionsFromService() failed, errorStatus = " + errorStatus + ", errorText = " + errorText;
            }
        };

        if (_requestDataType != "jsonp") {
            ajaxParameters.data = "{" + dataParameter + "}";
        }
        else {
            //  Tip: when using jsonp requests data must be an object, not an string
            //  TODO: security issue with "eval()", use some "JSON.parse()" function instead of "eval()".
            ajaxParameters.data = eval("({" + dataParameter + "})");

            if (_requestJsonpCallbackParameter) {
                //  Overriding default "callback" function
                ajaxParameters.jsonp = _requestJsonpCallbackParameter;
            }
        }

        _requestCounter++;

        jQuery.ajax(
            ajaxParameters
        );

    };

    this.HideContainerTimeout = function() {
        thisObject.TraceMessage("BlurEvent(). setTimeout(). showing: " + _showing +
                ", thisObject.GetContainer().children().length: " + thisObject.GetContainer().children().length);
        if (!_showing) {
            if (_suggestionBehavior == "combo" && thisObject.GetContainer().children().length > 0) {
                //  "Combo" mode and only one possible suggestion ==> set this one
                _selectedSuggestion = 0;
                thisObject.ApplySelectedSuggestion(_selectedSuggestion);
            }

            thisObject.HideContainer();
        }
    };

    this.BlurEvent = function() {
        thisObject.TraceMessage("BlurEvent(). ID: " + jQuery(this).attr("id"));
        //setTimeout("this.HideContainerTimeout;", _lostFocusTimeout);
        setTimeout(function() {
            thisObject.TraceMessage("BlurEvent(). setTimeout(). showing: " + _showing +
                ", thisObject.GetContainer().children().length: " + thisObject.GetContainer().children().length);
            if (!_showing) {
                if (_suggestionBehavior == "combo" && thisObject.GetContainer().children().length > 0) {
                    //  "Combo" mode and only one possible suggestion ==> set this one
                    _selectedSuggestion = 0;
                    thisObject.ApplySelectedSuggestion(_selectedSuggestion);
                }

                thisObject.HideContainer();
            }
        }, _lostFocusTimeout);
    };

    this.StartKeyupListener = function() {
        this.TraceMessage("StartKeyupListener");
        //  To use inside jQuery expressions due to "this" ambiguity
        var thisObject = this;

        var inputControl = this.GetInputControl();

        //  Add on blur event
        inputControl.blur(thisObject.BlurEvent);

        //  Add on key up listener
        inputControl.keyup(thisObject.KeyupEvent);

    };
}

//  Suggestions startup routine
SuggestionsHandler.prototype.Startup = function() {
    this.TraceMessage("Startup. ID: " + this.GetInputControl().attr("id"));
    //  To use inside jQuery expressions due to "this" ambiguity
    var thisObject = this;

    //  Create styles if necessary
    this.AddStyles();

    var container = this.CreateContainer();

    //  Create container DIV (hidden)
    jQuery(container).hide().appendTo("body");

    this.GetInputControl().attr('autocomplete', 'off');

    //  Mark that element has the plugin attached to it
    if (!this.GetInputControl().hasClass('_suggestionsPluginAttached')) {
        this.GetInputControl().addClass('_suggestionsPluginAttached');
    }

    this.SaveAndUnbindEvents();
    this.StartKeyupListener();
};

//        //  Pause suggestions 
//        SuggestionsHandler.prototype.Pause = function()
//        {
//        };

//  Suggestions shutdown routine
SuggestionsHandler.prototype.Shutdown = function() {
    this.GetInputControl().removeClass('_suggestionsPluginAttached');

    this.RestoreSavedEvents();
    this.GetContainer().remove();
    this.RemoveStyles();
};

SuggestionsHandler.prototype.GetTargetElement = function() {
    return this.thisObject;
};

SuggestionsHandler.prototype.WriteTraceMessage = function(message) {
    this.TraceMessage(message);
};

SuggestionsHandler.prototype.IsAlreadyAttachedTo = function(targetElement) {
    return $(targetElement).hasClass('_suggestionsPluginAttached');
};

//        //  Get suggestions
//  Show suggestions
SuggestionsHandler.prototype.ShowSuggestions = function(suggestions) {
    //            if (this._suggestionBehavior == "classic") {
    var alteredSuggestions = this.PreProcessSuggestions(suggestions);

    this.ShowSuggestionsClassic(alteredSuggestions);
    //            }
    //            else {
    //                this.ShowSuggestionsCombo(suggestions);
    //            }
}

//=============================================================================================================
//====  jQuery suggestions plug-in definition

//  All suggestion handlers on page, one per control, key is element id (e.g. myTextBox.id)
var _suggestionsHandlers = new Object();

//  DOM ready
//jQuery(document).ready(function() {

    //====  jQuery callWithinScope plug-in definition
    //  Sometimes it's necessary to explicitly bind a method to it's scope (e.g. a method to it's object)
    //  it can be done by using this plug to build the method.
    //  E.g. use "$.callWithinScope(myObject, 'myObjectsMethod')" instead of "myObject.myObjectsMethod"
    jQuery.callWithinScope = function(scope, method) {
        if (typeof (method) == "string") {
            //  Method was provided by name (string)

            //  If no scope was specified, gloabl (windows) scope is used
            scope = scope || window;

            if (!scope[method]) {
                throw "jQuery.callWithinScope plug-in: Method \"" + method + "\" does not exist";
            }

            return function() {
                return scope[method].apply(scope, arguments || []);
            };
        }
        else {
            //  Method was provided by itself

            return !scope ? method : function() {
                return method.apply(scope, arguments || []);
            };
        }
    };

    //====  jQuery suggestions plug-in definition
    //  Extend jQuery in order to do $("#textboxId").suggest() and attach all sugestion functionality to the control
    jQuery.fn.extend({
        unsuggest: function() {
            return this.each(function() {
                if (_suggestionsHandlers[this.id] != null) {
                    _suggestionsHandlers[this.id].Shutdown();
                    delete _suggestionsHandlers[this.id];
                }
            });
        },
        suggest: function(parameters) {

            //  Add new one
            return this.each(function() {
                //  Check for id tag, it's mandatory
                if (typeof (this.id) == "undefined" || !this.id || this.id == "") {
                    throw "jQuery.Suggestions plug-in: target control must have a defined id";
                }

                var overrideParameter;

                if (typeof (parameters.override) != "undefined") {
                    overrideParameter = parameters.override;
                }
                else {
                    overrideParameter = SuggestionsHandler.defaultParameters.override;
                }

                if (overrideParameter || !_suggestionsHandlers[this.id] || !_suggestionsHandlers[this.id].IsAlreadyAttachedTo(this)) {
                    //  Remove previous suggestion if existent
                    jQuery(this).unsuggest();

                    //  Attach plugin
                    _suggestionsHandlers[this.id] = new SuggestionsHandler(this, parameters);
                    _suggestionsHandlers[this.id].Startup();
                }
            });
        }
    });

    //  Extensio to set up default parameter values
    jQuery.suggestSetup = function(parameters) {
        //  Set default parameters
        for (var member in parameters) {
            SuggestionsHandler.defaultParameters[member] =
                    parameters[member]; // || SuggestionsHandler.defaultParameters[member];
        }
    };

    //  Now set up default parameter values
    jQuery.suggestSetup({
        override: false,                //  true: always attach new plugin instance
        //  false: if plugin is already attached to control, leave existent instance

        behavior: "classic",            //  "classic": google like suggestions, "combo": suggestions combo box (ddl)

        minimumInputLength: 1,
        maximumResults: -1,             //  No limit
        suggestionField: "d",           //  ASP.Net page methods return a "d" member of the ajax result 
        suggestionTypeField: "Type",
        suggestionTextField: "Text",
        suggestionKeyField: "Key",
        suggestionUrlField: "Url",
        optimizeRequests: true,
        showSuggestionsTimeout: 400,    //  Base timeout after keyup event
        lostFocusTimeout: 180,          //  Timeout to hide suggestions when input looses focus

        //  Custom handlers
        onLoad: null,   //  function(suggestions) { return suggestions; }

        //  Trim text and add "..." if text size overflows the container
        limitTextSize: true,

        //  Css classes (not defined by default)
        cssClassContainer: null,
        cssClassSelectedItem: null,
        cssClassUnSelectedItem: null,

        //  Styles
        cssStyleContainer: "border: 1px solid #A1A1A1;",
        cssStyleSelectedItem: "text-align: left; background-color: #F1F1F1;",
        cssStyleUnSelectedItem: "text-align: left; background-color: white;",

        //  Container position and width offset
        containerTopOffset: 3,
        containerLeftOffset: 0,
        containerWidthOffset: 1,

        //  Ajax request parameters
        requestType: "POST",
        requestUrl: "Services/Suggestions/SuggestionsAjaxService.asmx/GetSuggestions",
        requestContentType: "application/json; charset=utf-8",
        requestDataTextField: "text",
        requestDataMaximumResultsField: "maxResults",
        requestDataType: "json"
    });

//});
