﻿
/**
* An autosuggest textbox control.
* @class
* @scope public
*/
function AutoSuggestControl(oTextbox /*:HTMLInputElement*/,
                            oHidden /*:HTMLHiddenElement*/,
                            oConjugateTextbox /*:HTMLInputElement*/,
                            oProvider /*:SuggestionProvider*/) {

    /**
    * The currently selected suggestions.
    * @scope private
    */
    this.cur /*:int*/ = -1;

    this.longJumpSize = 12; // the number of items to skip when PageUp/PageDown are pressed

    /**
    * The dropdown list view.
    * @scope private
    */
    this.layer = null;

    /**
    * The dropdown list layer.
    * @scope private
    */
    this.innerLayer = null;

    /**
    * Suggestion provider for the autosuggest feature.
    * @scope private.
    */
    this.provider /*:SuggestionProvider*/ = oProvider;

    /**
    * The textbox to capture.
    * @scope private
    */
    this.textbox /*:HTMLInputElement*/ = oTextbox;

    /**
    * The textbox to capture.
    * @scope private
    */
    this.code /*:HTMLHiddenElement*/ = oHidden;

    /**
    * The textbox to look for suggestion exclusions (e.g. tbFrom is conjugate for tbTo)
    * @scope private
    */
    this.conjugateTextbox = oConjugateTextbox;

    /**
    * The boolean value indicating opened suggestion
    * @scope private
    */
    this.isOpened /*:bool*/ = false;

    //initialize the control
    this.init();

}

/**
* Autosuggests one or more suggestions for what the user has typed.
* If no suggestions are passed in, then no autosuggest occurs.
* @scope private
* @param aSuggestions An array of suggestion strings.
* @param bTypeAhead If the control should provide a type ahead suggestion.
*/
AutoSuggestControl.prototype.autosuggest = function (aSuggestions /*:Array*/) {

    //make sure there's at least one suggestion
    if (aSuggestions.length > 0) {
        this.showSuggestions(aSuggestions);
    } else {
        this.hideSuggestions();
    }
};

/**
* Creates the dropdown layer to display multiple suggestions.
* @scope private
*/
AutoSuggestControl.prototype.createDropDown = function () {

    var oThis = this;

    //create the layer and assign styles
    this.layer = document.createElement("div");
    this.layer.className = "suggestions";
    this.layer.id = this.textbox.id + "_suggestions";
    this.layer.style.display = "none";
    this.innerLayer = document.createElement("div");
    this.innerLayer.className = "townsB cities";
    this.layer.appendChild(this.innerLayer);

    var closeLayer = document.createElement("div");
    closeLayer.className = "townsB";
    this.layer.appendChild(closeLayer);
    var oPar = document.createElement("p");
    oPar.appendChild(document.createTextNode("[x] "));
    var oUnder = document.createElement("u");
    oUnder.appendChild(document.createTextNode("закрыть"));
    oPar.appendChild(oUnder);
    closeLayer.appendChild(oPar);
    //when the user clicks on the a suggestion, get the text (innerHTML)
    //and place it into a textbox
    this.innerLayer.onmousedown =
    this.innerLayer.onmouseup =
    this.innerLayer.onmouseover = function (oEvent) {
        oEvent = oEvent || window.event;
        oTarget = oEvent.target || oEvent.srcElement;

        if (!oThis.isCursorOverClientArea())
            return;

        if (oEvent.type == "mousedown") {
            oThis.textbox.value = oTarget.title.split("|")[0];
            oThis.code.value = oTarget.title.split("|")[1];
            oThis.hideSuggestions();
        } else if (oEvent.type == "mouseover") {
            oThis.highlightSuggestion(oTarget);
        } else {
            oThis.textbox.focus();
        }
    };

    document.body.appendChild(this.layer);
};

/**
* Gets the left coordinate of the textbox.
* @scope private
* @return The left coordinate of the textbox in pixels.
*/
AutoSuggestControl.prototype.getLeft = function () /*:int*/{

    var oNode = this.textbox;
    var iLeft = 0;

    while (oNode != null) {
        iLeft += oNode.offsetLeft;
        oNode = oNode.offsetParent;
    }

    return iLeft;
};

/**
* Gets the top coordinate of the textbox.
* @scope private
* @return The top coordinate of the textbox in pixels.
*/
AutoSuggestControl.prototype.getTop = function () /*:int*/{

    var oNode = this.textbox;
    var iTop = 0;

    while (oNode != null) {
        iTop += oNode.offsetTop;
        oNode = oNode.offsetParent;
    }

    return iTop;
};

/**
* Handles three keydown events.
* @scope private
* @param oEvent The event object for the keydown event.
*/
AutoSuggestControl.prototype.handleKeyDown = function (oEvent /*:Event*/) {
    var handled = false;

    switch (oEvent.keyCode) {
        case 33: // page up
            this.previousSuggestion(this.longJumpSize);
            handled = true;
            break;
        case 34: // page down
            this.nextSuggestion(this.longJumpSize);
            handled = true;
            break;
        case 38: // up arrow
            this.previousSuggestion();
            handled = true;
            break;
        case 40: // down arrow 
            this.nextSuggestion();
            handled = true;
            break;
        case 13: // enter
            this.hideSuggestions();
            handled = true;
            break;
        default:
            break;
    }

    if (handled) {
        if (oEvent.stopPropagation)
            oEvent.stopPropagation();
        return false;
    } else {
        return true;
    }
};

/**
* Handles keyup events.
* @scope private
* @param oEvent The event object for the keyup event.
*/
AutoSuggestControl.prototype.handleKeyUp = function (oEvent /*:Event*/) {

    var iKeyCode = oEvent.keyCode;

    //for backspace (8) and delete (46), shows suggestions without typeahead
    if (iKeyCode == 8 || iKeyCode == 46) {
        this.provider.requestSuggestions(this);

        //make sure not to interfere with non-character keys
    } else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode < 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
        //ignore
    } else {
        //request suggestions from the suggestion provider with typeahead
        this.provider.requestSuggestions(this);
    }
};

/**
* Handles mouseDoun events.
* @scope private
* @param oEvent The event object for the keyup event.
*/
AutoSuggestControl.prototype.handleMouseDown = function (oEvent /*:Event*/) {
    if (!this.isOpened) {
        this.showSuggestions(this);
        this.provider.requestSuggestions(this);
    } else
        this.hideSuggestions();
};


/**
* Hides the suggestion dropdown.
* @scope private
*/
AutoSuggestControl.prototype.hideSuggestions = function () {
    this.isOpened = false;
    $("#" + this.layer.id).slideUp("normal");
};

/**
* Highlights the given node in the suggestions dropdown.
* @scope private
* @param oSuggestionNode The node representing a suggestion in the dropdown.
*/
AutoSuggestControl.prototype.highlightSuggestion = function (oSuggestionNode) {
    for (var i = 0; i < this.innerLayer.childNodes.length; i++) {
        var oNode = this.innerLayer.childNodes[i];
        if (oNode == oSuggestionNode) {
            oNode.className = "current";
        } else if (oNode.className == "current") {
            if (i != this.innerLayer.childNodes.length - 1)
                oNode.className = "town";
            else
                oNode.className = "townLast";
        }
    }
};

/**
* Initializes the textbox with event handlers for
* auto suggest functionality.
* @scope private
*/
AutoSuggestControl.prototype.init = function () {

    //save a reference to this object
    var oThis = this;

    //assign the onMouseDown event handler
    $(this.textbox).bind("mousedown", function (oEvent) {
        oThis.handleMouseDown(oEvent);
        return true;
    });

    //assign the onkeyup event handler
    $(this.textbox).bind("keyup", function (oEvent) {
        //check for the proper location of the event object
        if (!oEvent)
            oEvent = window.event;

        //call the handleKeyUp() method with the event object
        oThis.handleKeyUp(oEvent);
    });

    //assign onkeydown event handler
    $(this.textbox).bind("keydown", function (oEvent) {
        //check for the proper location of the event object
        if (!oEvent)
            oEvent = window.event;

        //call the handleKeyDown() method with the event object
        return oThis.handleKeyDown(oEvent);
    });

    //assign onblur event handler (hides suggestions)    
    $(this.textbox).bind("blur", function () {
        if (oThis.isCursorOverSuggestionWindow()) {
            oThis.textbox.focus();
        } else
            oThis.hideSuggestions();
    });

    //create the suggestions dropdown
    this.createDropDown();
};

/**
* Highlights the next suggestion in the dropdown and
* places the suggestion into the textbox.
* @scope private
*/
AutoSuggestControl.prototype.nextSuggestion = function (jumpSize) {
    if (!jumpSize)
        jumpSize = 1;

    var cSuggestionNodes = this.innerLayer.childNodes;
    if (this.cur >= cSuggestionNodes.length)
        this.cur = -1;

    if (cSuggestionNodes.length > 0) {
        var newCur = Math.min(this.cur + jumpSize, cSuggestionNodes.length - 1);
        if (newCur == this.cur)
            return;

        this.cur = newCur;
        var oNode = cSuggestionNodes[this.cur];
        this.highlightSuggestion(oNode);
        this.adjustScrollPosition(oNode);
        this.textbox.value = oNode.title.split("|")[0];
        this.code.value = oNode.title.split("|")[1];
    }
};

/**
* Highlights the previous suggestion in the dropdown and
* places the suggestion into the textbox.
* @scope private
*/
AutoSuggestControl.prototype.previousSuggestion = function (jumpSize) {
    if (!jumpSize)
        jumpSize = 1;

    var cSuggestionNodes = this.innerLayer.childNodes;
    if (this.cur >= cSuggestionNodes.length)
        this.cur = -1;

    if (cSuggestionNodes.length > 0) {
        var newCur = Math.max(this.cur - jumpSize, 0);
        if (newCur == this.cur)
            return;

        this.cur = newCur;
        var oNode = cSuggestionNodes[this.cur];
        this.highlightSuggestion(oNode);
        this.adjustScrollPosition(oNode);
        this.textbox.value = oNode.title.split("|")[0];
        this.code.value = oNode.title.split("|")[1];
    }
};

/**
* Selects a range of text in the textbox.
* @scope public
* @param iStart The start index (base 0) of the selection.
* @param iLength The number of characters to select.
*/
AutoSuggestControl.prototype.selectRange = function (iStart /*:int*/, iLength /*:int*/) {

    //use text ranges for Internet Explorer
    if (this.textbox.createTextRange) {
        var oRange = this.textbox.createTextRange();
        oRange.moveStart("character", iStart);
        oRange.moveEnd("character", iLength - this.textbox.value.length);
        oRange.select();

        //use setSelectionRange() for Mozilla
    } else if (this.textbox.setSelectionRange) {
        this.textbox.setSelectionRange(iStart, iLength);
    }

    //set focus back to the textbox
    this.textbox.focus();
};

/**
* Builds the suggestion layer contents, moves it into position,
* and displays the layer.
* @scope private
* @param aSuggestions An array of suggestions for the control.
*/
AutoSuggestControl.prototype.showSuggestions = function (aSuggestions /*:Array*/) {

    var oHyper = null;
    this.cur = -1;
    this.isOpened = true;
    this.innerLayer.innerHTML = "";  //clear contents of the layer

    for (var i = 0; i < aSuggestions.length; i++) {
        oHyper = document.createElement("a");
        var href = document.createAttribute('href');
        href.nodeValue = "#";
        oHyper.attributes.setNamedItem(href);
        var click = document.createAttribute('onclick');
        click.nodeValue = "return false;";
        oHyper.attributes.setNamedItem(click);
        oHyper.title = aSuggestions[i].Name + "|" + aSuggestions[i].IATA;

        var suggestion = aSuggestions[i];
        oHyper.appendChild(document.createTextNode(suggestion.Name + " " + suggestion.IATA));
        this.innerLayer.appendChild(oHyper);
    }

    this.layer.style.left = (this.getLeft() - 3) + "px";
    this.layer.style.top = (this.getTop() + this.textbox.offsetHeight + 4) + "px";
    $("#" + this.layer.id).slideDown("normal");

};

AutoSuggestControl.prototype.showLoadingPanel = function () {
    this.innerLayer.innerHTML = "<div class='loading'>Загрузка...<div>";
}

var absCursorCoords = {
    x: void (0),
    y: void (0)
};

$(document).mousemove(function (e) {
    absCursorCoords.x = e.pageX;
    absCursorCoords.y = e.pageY;
});

AutoSuggestControl.prototype.isCursorOverSuggestionWindow = function () {
    return this.isCursorOver(false);
};

AutoSuggestControl.prototype.isCursorOverClientArea = function () {
    return this.isCursorOver(true);
};

AutoSuggestControl.prototype.getGeomData = function () {
    var extra =
            1 /* wrapper's border */ +
            1 /* wrapper's padding */ +
            1 /* textbox'es border */;

    var geomData = {
        scrollBarWidth: 16,
        cursorX: absCursorCoords.x,
        cursorY: absCursorCoords.y,
        cityListAbsLeft: this.getLeft() - extra,
        cityListAbsTop: this.getTop() + extra + this.textbox.offsetHeight + extra,
        cityListWidth: this.innerLayer.offsetWidth,
        cityListHeight: this.innerLayer.offsetHeight
    };

    return geomData;
};

AutoSuggestControl.prototype.isCursorOver = function (includeScrollBar) {
    var geomData = this.getGeomData();

    if (!(geomData.cursorX && geomData.cursorY))
        return false;

    var isCursorOver =
        geomData.cursorX >= geomData.cityListAbsLeft &&
        geomData.cursorX <= (
                geomData.cityListAbsLeft + geomData.cityListWidth -
                (includeScrollBar ? 1 : 0) * geomData.scrollBarWidth
        ) &&
        geomData.cursorY >= geomData.cityListAbsTop &&
        geomData.cursorY <= (geomData.cityListAbsTop + geomData.cityListHeight);

    return isCursorOver;
};

AutoSuggestControl.prototype.adjustScrollPosition = function (selectedCityNode) {
    var geomData = this.getGeomData();
    var cityList = selectedCityNode.parentNode;
    var nodeRelativeTop = selectedCityNode.offsetTop;
    var nodeHeight = selectedCityNode.offsetHeight;
    var nodeRelativeBottom = nodeRelativeTop + nodeHeight;

    var selectedCityNodeIsAboveCityListVisibleArea =
        nodeRelativeTop < cityList.scrollTop;
    if (selectedCityNodeIsAboveCityListVisibleArea)
        cityList.scrollTop = nodeRelativeTop;

    var selectedCityNodeIsBeyondCityListVisibleArea =
        nodeRelativeBottom > cityList.scrollTop + geomData.cityListHeight;
    if (selectedCityNodeIsBeyondCityListVisibleArea)
        cityList.scrollTop = nodeRelativeBottom - geomData.cityListHeight;
};
