The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
// ------------------------------------------------------------------
// Copyright 2007-2008 Philip Gwyn.  All rights reserved.
//
// Allow multi-key selection of elements in a menulist.
// ------------------------------------------------------------------

// ------------------------------------------------------------------
function MultiKeypress ( textbox, menulist_id, timeout, width ) {
    this.textbox = textbox;
    this.menulist_id   = menulist_id;
    this.menulist = $( menulist_id );
    if( !timeout )
        timeout = 2;
    this.timeout = timeout;
    if( !width ) 
        width = 2;
    this.width = width;
    this.pressed = '';
    this.new_index = -1;
    this.changed = 0;

    var self = this;
//    this.menulist.addEventListener( 'focus', 
//                    function (e) { self.focus_menulist( e ) }, true );
    this.menulist.addEventListener( 'command', 
                    function (e) { self.command_menulist( e ) }, true );
    this.menulist.addEventListener( 'keypress', 
                    function (e) { self.keypress_menulist( e ) }, true );
    this.textbox.addEventListener( 'keypress', 
                    function (e) { self.keypress_textbox( e ) }, true );
}

var _ = MultiKeypress.prototype;


// ------------------------------------------------------------------
// Deal with a keypress on the menulist
_.keypress_menulist = function ( event ) {

    // Stop any timeout from happening
    if( this.tID ) {
        window.clearTimeout( this.tID );
        delete this['tID'];
    }

    var code = event.charCode ? event.charCode : event.which;
    if( code != 0 && event.keyCode != 9 ) {
        return this.keypress( event );
    }
    return true;
}

// ------------------------------------------------------------------
// Deal with a keypress on the textbox
_.keypress_textbox = function (event) {
    
    if( event.keyCode == 9 ) {   // tab
        fb_log( "Multikeypress tab" );
        // we want to prevent the focus from going to the menulist
        // going forwards => one extra advanceFocus()
        if( !event.shiftKey )
            document.commandDispatcher.advanceFocus();
        // backwards -> do nothing
        return true;
    }

    return this.keypress( event );
}

// ------------------------------------------------------------------
// Deal with a user's keypress
_.keypress = function (event) {
    
    var code = event.charCode ? event.charCode : event.which;
    var s, shorter;
    this.pressed = this.textbox.value;

    // other control key
    if( code == 0 || event.altKey || event.ctrlKey || event.metaKey ) {    
        fb_log( "code=" + code );
        return true;
    }
    // Backspace
    else if( code == 8 ) {
        if( this.textbox.selectionStart == 0 ) 
            return false;
        this.pressed = this.pressed.substr( 0, this.textbox.selectionStart-1 )
                        + 
                       this.pressed.substr( this.textbox.selectionEnd );
    
        this.textbox.selectionEnd = this.textbox.selectionStart 
        s = this.textbox.selectionStart -1 ;
        shorter = true;
    }
    // A letter
    else {
        var before = this.pressed;
        this.pressed = this.pressed.substr( 0, this.textbox.selectionStart )
                       + String.fromCharCode( code ) +
                       this.pressed.substr( this.textbox.selectionEnd );
        if( this.pressed != before ) {
            // get this change to the server sooner
            this.changed = 1;
        }
        s = this.textbox.selectionStart + 1;
    }
    this.drop_timeout();

    this.textbox.value = this.pressed;
    this.textbox.selectionEnd = this.textbox.selectionStart = s;

    // Go to the new item
    this.select_item( shorter );

    // Dispite these, mozilla's internal key handler is called.  Grrr.
    event.preventDefault();
    event.stopPropagation();

    // Create a new timeout
    this.create_timeout();
    return false;
}

// ------------------------------------------------------------------
_.create_timeout = function ( now ) {
    var multi = 1000;
    if( this.changed ) {
        multi = 500;
        if( this.textbox.value.length == 2 || this.changed > 2 ) {
            now = 1;
        }
    }
    if( now ) {
        this.changed = 3;   // get the change to the server right now
        multi = 100;
    }
    this.start_timeout( multi );
    return false;
}

// ------------------------------------------------------------------
// 
_.start_timeout = function ( multi ) {
    fb_log( "suppressonselect=%s",
            this.menulist.getAttribute( 'suppressonselect' )
          );
    fb_log( "timeout multiplier=" + multi );
    var self = this;
    this.tID = window.setTimeout( function () { self.timedout() }, 
                                  this.timeout * multi 
                                );
    return false;
}



// ------------------------------------------------------------------
// When the wait times out, the pressed buffer is reset to empty and
// the new item is sent to the server
_.timedout = function () {
    this.pressed = '';
    delete this['tID'];

    if( this.new_index != -1 )      // select the new item
        this.select( this.new_index );

    // sync the input to the drop down 
    var len = 2;
    if( this.textbox.value.length != len ) {
        this.textbox.value = 
                this.menulist.selectedItem.label.substr(0, len);
    }

    this.new_index = -1;

    this.submit();
}

// ------------------------------------------------------------------
// Stop any timeout from happening
_.drop_timeout = function () {
    if( this.tID ) {
        window.clearTimeout( this.tID );
        delete this['tID'];
    }
}


// ------------------------------------------------------------------
// Find the item that best matches the pressed buffer.
_.select_item = function ( shorter ) {

    fb_log( "pressed='" + this.pressed + "'" );

    var menumenulist = this.menulist;
    if( this.pressed == '' ) {
        return;
    }

    var items = menumenulist.menupopup.childNodes;
    this.new_index = -1;
    var maybe = -1;
    var pressed = this.pressed.toLowerCase();
    var w = 1;
    if( shorter ) {
        if( menumenulist.selectedItem.label.substr(0,pressed.length).toLowerCase()
            == pressed ) {
            fb_log( "no change" );
            return;
        }
    }

    while( w <= pressed.length ) {
        var match = 0;
        for( var q = (maybe < 0 ? 0 : maybe ) ; 
                 q < items.length && ! match;
                 q++ ) {

            var text = items[q].label.toLowerCase();
            var p0 = pressed.substr( 0, w );
            var t0 = text.substr( 0, w );
            if( p0 == t0 ) {
                maybe = q;
                match = 1;
                // fb_log( "match " + p0 + ">=" + t0 + " label=" + items[maybe].label );
            }
            else if( p0 > t0 ) {
                maybe = q;
                // fb_log( "maybe " + p0 + ">=" + t0 + " label=" + items[maybe].label );
            }
            else {
                // fb_log( "not " + p0 + ">=" + t0 );
            }
        }
        w++;
    }

    if( maybe > -1 ) {
        fb_log( "match " + items[maybe].label );
        this.new_index = maybe;
        this.select( this.new_index );
    }
}

// ------------------------------------------------------------------
// Setting selectedItem randomly triggers onChange.  So we do it explictly.
_.submit = function () {

    this.changed = 0;
    if( $application ) {
        fb_log( "submit #" + this.menulist.selectedIndex );
        
        var pop = this.menulist.childNodes[0];
        var target = pop.childNodes[ this.menulist.selectedIndex ];
        var e = { target: target };
        $application.fireEvent_Command( e );
    }
}


// ------------------------------------------------------------------
// It still does it's fucking around, despite the preventDefault()
// So the selection is done at timeout, when it's time to submit the
// new value
_.select = function ( new_index ) {
    fb_log( "want #" + new_index );
    if( this.menulist.selectedIndex != new_index ) {
        // Get this change to the server fast
        this.changed = 2;
    }
    this.menulist.selectedIndex = new_index;
    fb_log( "selected #" + this.menulist.selectedIndex );
    this.menulist.selectedItem = this.menulist.menupopup.childNodes[new_index];
    fb_log( "select label=" + this.menulist.menupopup.childNodes[new_index].label );

//    this.textbox.value = this.menulist.selectedItem.label.substr( 0, 2 );
}

// ------------------------------------------------------------------
// Wouldn't it be nice to know what element had focus previously? 
// Because we don't, we don't know when to skip this element.  90% of
// users won't know about shift-tab anyway
_.focus_menulist = function (event) {
}

// ------------------------------------------------------------------
// If the user selects something in the menulist, we want to update 
// the textbox
_.command_menulist = function (event) {

    this.textbox.value = event.target.value;
    return;

    var source = event.target;
    var text = source.label;
    fb_log( "select=" + text );
    this.textbox.value = source.substr( 0, this.width );
}