if( typeof( Widget ) == 'undefined' ) { Widget = {}; }
if( typeof( Widget.Text ) == 'undefined' ) { Widget.Text = {}; }

// class constructor
Widget.Text.Suggest = function( oProps )
{
    // class properties
    this.props = {};

    // defaults
    this.props.inputId = 'search';
    this.props.divId = 'suggestions';
    this.props.requestHandler = undefined;
    this.props.requestUri = null;
    this.props.maxSuggestions = 10;

    // check if the AJAX library is included
    // this is the default one used for make the request
    if( typeof( AJAX ) != 'undefined' )
    {
        this.props.requestHandler = new AJAX();
    }

    // overwrite properties with user definitions
    for( var cProp in this.props )
    {
        if( oProps[cProp] != undefined )
        {
            this.props[cProp] = oProps[cProp];
        }
    }

    // inject user defined functions for make the request and parse the response
    if( oProps.request )
    {
        this.request = oProps.request;
    }

    if( oProps.parseResponse )
    {
        this.parseResponse = oProps.parseResponse;
    }

    // DOM stuff
    this.dom = {};

    // search-related stuff
    this.search = {};
    this.search.timer = 0;
    this.search.lastQuery = '';
    this.search.suggestionSelectedIndex = undefined;
    this.search.suggestions;

    // initialize!
    this._init();
}

// version
Widget.Text.Suggest.VERSION = '0.01';

// exported methods
Widget.Text.Suggest.EXPORT = [ 'request', 'parseRequest' ];

// class methods
Widget.Text.Suggest.prototype = {

// must be called when page is fully loaded
_init: function()
{
    var oWTS = this;

    // load search field element
    var oSearchBox = document.getElementById( this.props.inputId );

    // disable browser autocomplete behaviour
    oSearchBox.setAttribute( 'autocomplete', 'off' );

    // add keyboard event handler on search box
    this._addEvent(
        oSearchBox,
        'keyup',
        function(e) { oWTS._keyUpHandler(e); }
    );

    this.dom.inputEl = oSearchBox;

    // include the search box in a div (this is the way I've found to make the whole thing work properly...)
    var oSearchBoxDiv = document.createElement( 'div' );

    var oSearchBoxParent = oSearchBox.parentNode;
    oSearchBoxParent.removeChild( oSearchBox );
    oSearchBoxDiv.appendChild( oSearchBox );
    oSearchBoxParent.appendChild( oSearchBoxDiv );
 
    // create suggestions div container element
    var oSuggestionsDiv = document.createElement( 'div' );
    oSuggestionsDiv.id = this.props.divId;

    // set style
    with( oSuggestionsDiv.style ) // TODO check if it's correct this way... add rules directly in the style sheet ?!
    {
        visibility = 'hidden';
        position = 'absolute';
        width = '250px';
        marginTop: '2px';
        backgroundColor = '#ffffff';
        border = '1px #d9d9d9 solid';
    }

    oSearchBox.form.appendChild( oSuggestionsDiv );

    this.dom.divEl = oSuggestionsDiv;
},

// rParams is an array literal which contains the search field name and the search field value
request: function( rParams )
{
    var oWTS = this;

    // request can be made only if a valid uri is supplied
    if( this.props.requestHandler && this.props.requestUri )
    {
        this.props.requestHandler.call(
            {
                uri: this.props.requestUri,
                callback: function( rResponse ) { oWTS.parseResponse( rResponse ); }
            },
            rParams
        );
    }
},

// rResponse is whatever comes from the request, this is the place where parse the response
parseResponse: function( rResponse )
{
    // default value returned by the server-side script is a string with list of suggestions that must be evaluated to obtain a js array object
    // ex: new Array( 'edoardo','sabadelli' )
    var aResponse = eval( rResponse );

    // _showSuggestions method requires a js array with the list of suggestions as the only parameter
    this._showSuggestions( aResponse );
},

// keyboard handler
_keyUpHandler: function(e)
{
    if( ! e && window.event )
    {
        e = window.event;
    }

    var cKeyPressed = ( e.charCode ) ? e.charCode
                                     : ( e.keyCode ) ? e.keyCode
                                                     : ( e.which ) ? e.which
                                                                   : 0;

    // custom behaviour for up/down arrows and enter keys
    if( cKeyPressed == 13 || cKeyPressed == 38 || cKeyPressed == 40 )
    {
        var lSuggestionSelected = this.search.suggestionSelectedIndex;
        var aSuggestions = this.search.suggestions;

        // enter key
        if( cKeyPressed == 13 && lSuggestionSelected != undefined )
        {
            this.dom.divEl.style.visibility = 'hidden';
        }
        // highligh a span element (suggestion) when up/down arrow keys are pressed and suggestions span elements are available
        else if( aSuggestions.length > 0 )
        {
            if( lSuggestionSelected != undefined )
            {
                // set appearance of previous element as unselected
                var oPrevSelected = aSuggestions[lSuggestionSelected];
                //oPrevSelected.className = 'suggestion';
                this._unselectSuggestion( oPrevSelected );
            }

            // up arrow key
            if( cKeyPressed == 38 )
            {
                if( ! lSuggestionSelected )
                {
                    lSuggestionSelected = aSuggestions.length - 1;
                }
                else if( lSuggestionSelected > 0 )
                {
                    lSuggestionSelected--;
                }
            }
            // down arrow key
            else if( cKeyPressed == 40 )
            {
                if( lSuggestionSelected == undefined || lSuggestionSelected == aSuggestions.length - 1 )
                {
                    lSuggestionSelected = 0;
                }
                else if( lSuggestionSelected < aSuggestions.length - 1 )
                {
                    lSuggestionSelected++;
                }
            }

            // valid element selection
            if( lSuggestionSelected != undefined )
            {
                // put the suggestion value in the search box and set appearance of the element as selected
                this._selectSuggestion( aSuggestions[lSuggestionSelected] );

                // store this index as the last selected
                this.search.suggestionSelectedIndex = lSuggestionSelected;
            }
        }

        // disable browser default behaviour XXX
        if( window.event )
        {
            e.cancelBubble = true;
            e.returnValue = false;
        }
        else
        {
            e.cancelBubble = true;
            e.returnValue = false;
            e.preventDefault();
            e.stopPropagation();
        }
    }
    else
    {
        var oWTS = this;

        // reset timeout if another key is pressed before getSuggestions() is fired
        if( this.search.timer )
        {
            clearTimeout( this.search.timer );
            this.search.timer = 0;
        }

        // avoid to run again getSuggestions() when form is submitted (enter pressed)
        if( cKeyPressed != 13 )
        {
            this.search.timer = setTimeout( function() { oWTS._getSuggestions(); }, 200 );
        }
    }
},

// run the suggestions search on server via AJAX
_getSuggestions: function()
{
    var cSearch = this.dom.inputEl.value;
    var oSuggestionsDiv = this.dom.divEl;

    // hide suggestions div and reset variables if no search string is available
    if( ! cSearch && oSuggestionsDiv )
    {
        oSuggestionsDiv.style.visibility = 'hidden';
        this.search.lastQuery = '';
        this.search.suggestionSelectedIndex = undefined;
    }
    // run suggestions search if search string is different from last one
    else if( cSearch != this.search.lastQuery )
    {
        this.search.lastQuery = cSearch;
        this.search.suggestionsSelectedIndex = undefined;

        // make the request to get the suggestions
        this.request([ this.dom.inputEl.name, cSearch ]);
    }
},

// build and show the list of suggestions
_showSuggestions: function( aResponse )
{
    var oWTS = this;

    var oSuggestionsDiv = this.dom.divEl;

    // remove all the previous suggestions (if present)
    if( this.search.suggestions )
    {
        // hide the suggestions div
        oSuggestionsDiv.style.visibility = 'hidden';

        var aSuggestionsDivs = this.search.suggestions;

        while( aSuggestionsDivs.length )
        {
            oSuggestionsDiv.removeChild( oSuggestionsDiv.lastChild );
        }
    }
    
    var aSuggestionsDivs = new Array();

    // set max number of suggestions to show
    var nMaxSuggestions = ( aResponse.length > this.props.maxSuggestions )
                        ? this.props.maxSuggestions
                        : aResponse.length;

    // create a <div> element for each suggestion
    for( var r = 0; r < nMaxSuggestions; r++ )
    {
        var oSuggestion = document.createElement( 'div' );
//        oSuggestion.className = 'suggestion';

        // set style
        with( oSuggestion.style )
        {
            paddingTop = 0;
            paddingRight = '4px';
            paddingBottom = 0;
            paddingLeft = '4px';
        }

        this._addEvent(
            oSuggestion,
            'mouseover',
            function(e) { oWTS._selectSuggestion( undefined, e ); }
        );
        this._addEvent(
            oSuggestion,
            'mouseout',
            function(e) { oWTS._unselectSuggestion( undefined, e ); }
        );
        this._addEvent(
            oSuggestion,
            'click',
            function() { oWTS.dom.divEl.style.visibility = 'hidden'; oWTS.dom.inputEl.focus(); }
        );

        oSuggestion.innerHTML = aResponse[r];

        aSuggestionsDivs.push( oSuggestion );

        oSuggestionsDiv.appendChild( oSuggestion );
    }

    if( aSuggestionsDivs.length > 0 )
    {
        oSuggestionsDiv.style.visibility = 'visible';
        this.search.suggestions = oSuggestionsDiv.childNodes;
    }

    // reset timeout
    this.search.timer = 0;
},

// used both for keyboard and mouse selection
_selectSuggestion: function( oSuggestion, e )
{
    if( ! oSuggestion )
    {
        if( ! e && window.event )
        {
            e = window.event;
        }

        oSuggestion = ( e.target ) ? e.target
                                   : ( e.srcElement ) ? e.srcElement
                                                      : undefined;

        // this is for a Safari bug (see w3cschools)
        if( oSuggestion && oSuggestion.type == 3 )
        {
            oSuggestion = oSuggestion.parentNode;
        }
    }

//    oSuggestion.className = 'suggestion selected';
    with( oSuggestion.style )
    {
        color = '#ffffff';
        backgroundColor = '#4682b4';
    }

    // put the suggestion value in the search box
    this.dom.inputEl.value = oSuggestion.firstChild.nodeValue;
},

_unselectSuggestion: function( oSuggestion, e )
{
    if( ! oSuggestion )
    {
        if( ! e && window.event )
        {
            e = window.event;
        }

        oSuggestion = ( e.target ) ? e.target
                                   : ( e.srcElement ) ? e.srcElement
                                                      : undefined;

        // this is for a Safari bug (see w3cschools)
        if( oSuggestion && oSuggestion.type == 3 )
        {
            oSuggestion = oSuggestion.parentNode;
        }
    }

//    oSuggestion.className = 'suggestion';
    with( oSuggestion.style )
    {
        color = 'inherit';
        backgroundColor = 'inherit';
    }

},

// cross-browser event handler
_addEvent: function( oObj, cEvent, rFunction )
{
    if( oObj.addEventListener )
    {
        oObj.addEventListener( cEvent, rFunction, false );
        return true;
    }
    else if( oObj.attachEvent )
    {
        return oObj.attachEvent( 'on' + cEvent, rFunction );
    }
    else
    {
        return false;
    }
}

};

/*

=head1 NAME

Widget.Text.Suggest - Add a suggest-while-you-type feature on text fields in a Google Suggest fashion

=head1 SYNOPSIS

  // Create a new Widget.Text.Suggest object
  var oWTS =
    new Widget.Text.Suggest(
      {
        requestUri: 'http://host.domain.tld/cgi-bin/ajax_script'
      }
    );

=head1 DESCRIPTION

This magic class adds the suggest feature to a form text field in a Google Suggest (L<http://labs.google.com/suggest>) fashion.
It is possible to transform a normal text field in an AJAX powered little application which grabs the suggestions and fills a <div> element generated
on the fly and attached just below the text field.
There is no need to change the XHTML code of the page, the only thing needed is a text field with a valid id attribute.

=head1 METHODS

=head2 B<new()>

Object constructor, returns a brand new Widget.Text.Suggest object.

=head3 Parameters

=over 3

=item B<oProps>

Structure with properties passed as an object literal.

Properties are:

=over 20

=item C<inputId>

ID of the search text field (default is search)

=item C<divId>

ID of the DIV element generated which will contain all the suggestions (default is suggestions)

=item C<requestHandler>

Object to use for handle the request (ex: AJAX, HTTP.Request ... ).
If an object is supplied here it will be used for all the requests without the need to instance a new object each time a request is made.

=item C<requestUri>

URI of the script or resource to use for the request B<mandatory!>

=item C<request>

Reference to the function for make the request (see the help about the request method).
If supplied overwrites the default method.

=item C<parseResponse>

Reference to the function for parse the response and build the list of suggestions to display (see the help about the parseResponse method).
If supplied overwrites the default method.

=item C<maxSuggestions>

Maximum number of suggestions to show (default is 10).
If the number of suggestions returned by the request is greater than this value, only the first C<maxSuggestions> are shown.

=back

=back

=head3 Returned value

=over 3

=item B<oWTS>

A Widget.Text.Suggest object related to the C<inputId> text field.

=back

=head2 B<request()>

This method handles the request for the suggestions.
It is possible to overwrite this method to define a custom behaviour.

=head3 Parameters

=over 3

=item B<rParams>

Array literal which contains the search field name and the current value.
Example: [ 'q', 'appl' ]

=back

=head3 Returned value

=over 3

=item B<none>

The control is passed to the parseResponse() method.

=back

=head2 B<parseResponse()>

This method can be overwritten to parse the response that comes from the request.
The value passed to the internal _showSuggestions() method must be a javascript array with the list of suggestions.
The last line of the method must call the _showSuggestions() method and pass the list of suggestions like this:

=over 3

this._showSuggestions( aResponse );

=back

=head3 Parameters

=over 3

=item B<rResponse>

Whatever is returned by the request(), can be plain text or XML.

=back

=head3 Returned value

=over 3

=item B<none>

The control must be passed to the internal _showSuggestions() method.

=back

=head1 EXPORTS

When used with C<JSAN> will export request() and parseResponse().

=head1 EXAMPLES

=head1 SEE ALSO

Official web page at L<http://www.sabadelli.it/edoardo/projects/javascript/widget.text.suggest>

JSAN L<http://openjsan.org/>

=head1 AUTHOR

Edoardo Sabadelli - L<http://www.sabadelli.it/edoardo>

=head1 COPYRIGHT

Copyright (c) 2007 Edoardo Sabadelli. All rights reserved.

This module is free software; you can redistribuite it and/or modify it
under the terms of the Artistic license.

=cut

*/

