ENTER not triggering onclick() event, mouse is... why?

Hallo all,
I’m trying to rebuild/imitate another site in Javascript (mostly just to edumacate myself) and I’ve run into something.

I have an absolutely-positioned anchor with an onclick event. If it’s clicked, a hidden <select> appears and opens. The focus() is moved to this select immediately.

I can arrow up and down through the select options, and I can click on any of those options with a mouse and trigger the onclick() event I’ve set on the options (I have also tried with setting the onclick event on the select itself, same problem). Hitting ENTER does not seem to trigger the event though (FF7/Debian for now). I have no idea why, since that should be the normal event in a dropdown select when the ENTER key is hit anyway, as far as I can tell.

Am I wrong in this?

Should I explicitly write an onkeyup and check for the Enter key and add an onclick there? (seems like overkill)

Here is the code I’m working with:
HTML


          <label for="refineSelect" id="refine"><span>Refine term:</span> <a href="#void" id="refa"> </a>
              <select id="refineSelect" name="refineSelect">
                <option value="foo">Foo</option>
                <option value="bar">Bar</option>
                <optgroup label="-- Try on --">
                  <option value="amazon">Amazon</option>
                  <option value="bing">Images - Bing</option>
                  <option value="google">Images - Google</option>
                  <option value="maps">Maps</option>
                  <option value="news">News</option>
                  <option value="wikipedia">Wikipedia</option>
                  <option value="youtube">YouTube</option>
                </optgroup>
                <optgroup label="-- Show all --">
                  <option value="baz">Baz</option>
                  <option value="quux">Quux</option>
                </optgroup>
              </select>
            </label>

I originally had the label outside the select and may move it back out; it was originally wrapped to try something else out. I don’t think it matters here but maybe??

The CSS isn’t anything special, the anchor is absolutely positioned and so is the select.

Javascript (currently leaving out the bits that are not part of this function):


(inside Object Foo)
init: function() {
       var refineAnchor = document.getElementById('refa'),
            s = document.getElementById('refineSelect'),
            q = document.getElementById('q');

        Basis.addClass(s, 'hidden');

        Basis.addEventListener(refineAnchor, 'click', Foo.refineClickListener(s, q)); 
        Basis.addEventListener(s, 'blur', Foo.sBlurListener); 

...
    },
...
    showRefineSelect: function(s, q) {
        var opt;
        Basis.removeClass(s, 'hidden');
        s.size = s.length;
        //loop through s's options and add click listeners
        for(var i=0; i<s.options.length; i++) {
            opt = s.options[i];
            opt.onclick = (function(opt,s,q){
                return function() {
                     Foo.submitParams(opt,s,q);
                 }
            })(opt,s,q);
        }
        s.focus();
    },

    submitParams: function(opt, s, q) {
        alert(q.nodeName + ', value is ' + q.value); //for now, my test to see that click event happened
    },

    hideRefineSelect: function(s) {
        s.size='1';
        Basis.addClass(s, 'hidden');
    },
       
...
    refineClickListener: function(s, q) {
        return function() {
            Foo.showRefineSelect(s, q);
        };
    },

    sBlurListener: function(event) {
        Foo.hideRefineSelect(this);
    }

So, when the anchor gets a click event (works with mouse or Enter keys), the select dropdown appears and focus is automatically moved to it. This all works (so far, not widely tested).

I can arrow around the options, which I want.

I can click an option with the mouse and see the result of my click event that I’ve placed on the options.
I cannot hit Enter and see the result of my click event. I see in my debugger that yes, each option is getting the click event assigned to it.

Before adding click events to the options I had tried the simpler add-click-event-to-select. This hadn’t worked originally which is why I thought maybe I needed to add directly to the options.

Is there something I’m obviously missing here? My brain is in a fog.

Also, I’m doing this partially to better learn Javascript. I don’t care if there’s some 500kb downloadable jQuery whatsit that does it all for me. That defeats the purpose really.

Thanks,
poes

The onclick event for <option>s is supported by Firefox but isn’t standard. Clicking an option will trigger the onchange event.

The onclick event for <option>s is supported by Firefox but isn’t standard. Clicking an option will trigger the onchange event.

Hm, let me try onchange as my event then. Thanks.

I also noticed while playing around with the loops that:
-I can’t add any more events (but maybe this is because the element doesn’t support them in the first place?)
-I can’t get the event itself if I wanted to do something with it.

But this I assume is all my not knowing how to correctly deal with loops and events.

*edit no event works. However, I know someone managed to write this in Javascript because the real site does work, with Javascript. But the code is heavily obfuscated so I can’t really see what they did.

<option>s don’t have event handlers. onchange/onclick event handlers should be applied to the <select> element.

Tried that too, the moment nothing happened on the options.

If I use a cut-down, tutorial example, I can add an onchange event to a select and it acts as I expect, where I need to hit Enter to select my choice.

But it doesn’t seem to do anything within my code context.

It could have to do with that I have the select first absolutely positioned and then forced open using size. Not sure.
I tried a
s.onchange = (function() {
return function() {alert(‘foo bar baz’);}
})();

and I get no response, while of course doing
s.onchange = function() {alert(‘fuzzbazz’);};
had it going off the moment it opened.

I’m going to see if I can get “normal” select behaviour without hiding it and letting it do what it would normally do, but then I’d have to figure out how to keep it working while hiding it away again.

I’ve only had a quick look through your code ans post but I think you are over-scienceing what you are trying to do. Also, you talk about links but I don’t see any in your html, so not sure what you are trying to do there.

Perhaps try a new approach. Start from scratch with just a bare <select> with no styling and get it working - something like this:

This code alerts the selected option and its value.

        <select name="mySel" onchange="doThis(this);">
            <option value="1" selected="selected">option 1</option>
            <option value="2">option 2</option>
            <option value="3">option 3</option>
            <option value="4">option 4</option>
        </select>

        <script type="text/javascript">
            function doThis(selElem){
                alert('You selected: '+selElem.options[selElem.selectedIndex].innerHTML+"\
"+'Selected value: '+selElem.value);
            }
        </script>

First get the above basic <select> to work and then build on it in small steps and test it. Don’t add anything new until each step works correctly. Basically try to KISS and test as you build the application.

<label for="refineSelect" id="refine"><span>Refine term:</span> [b]<a href="#void" id="refa"> </a>[/b]

It’s a bit of a usability sin but the design isn’t mine. Trying to make a “button” open a dropdown select and then make it work is automatically a case of “over-engineering”, but on the other hand it’s something I feel I should fairly easily be able to wrangle myself.

Perhaps try a new approach. Start from scratch with just a bare <select> with no styling and get it working - something like this:

This code alerts the selected option and its value.

I have that (on another page). As I said, there are a bunch of tutorials showing this simple setup and I can copy/implement it no problem.

First I’m going to stop the label wrapping, since I don’t need that anymore (that was for an earlier attempt).
Then I’m going to stop Javascript from setting the size attribute, since that’s the main difference I seem to see between simple code and broken code.

Then, I hope there’s an obvious difference.

ok, but I think you’re doing this the hard way - removing bits and pieces to hopefully fix it rather than start with just a simple basic element, getting that working and then add functionality to it in stages making sure each stage works before adding the next one.

Well, it was indeed the size setting. Which means there was some much-extra engineering on the original page, because the original page allows one to arrow through the select options.

If size is greater than one on a select with an onchange, my browsers assume up/down arrowing is a change event. Which is wrong, and completely not what they do if size is defaulted to “1”. So, this is why things are okay with a mouse currently, but if you need to use keyboard you will end up selecting everything you arrow through.

I may have to give up on this one, because this behaviour is the same everywhere, even on simple
<select name=“foo” id=“foo” size=“5”>
<option value=“bar”>bar</option>
<option value=“bar2”>bar2</option>
<option value=“bar3”>bar3</option>
<option value=“bar4”>bar4</option>
<option value=“bar5”>bar5</option>
</select>

var s = document.getElementById(‘foo’);
s.onchange=function() {
alert(s.options[s.selectedIndex].value;);
}

remove the size and it works fine. However, you cannot “open” a select element with Javascript. Hence the size attribute.

After a bazillion hours of trial and error (which seems to be the ONLY method that works I swear), I got it doing what I want it to… at least, on Firefox. Intense trepidation on testing other browsers. But I think I got somewhere.

To recap: the point of the script was to take a (fake) button, and when it’s clicked, show an open select with options. If users are using a mouse, they can choose an option and Something Happens. If they are using the keyboard, however, they should be able to arrow around the select without Something Happening and hit ENTER to make their selection and make Something Happen.

In BOTH cases, if the user simply blurs (leaves the select either by clicking elsewhere on the page or hitting TAB), no selection is made. This allows users to scroll around the options, but choose not to choose one.

HTML:


          <label for="term">An input: </label>
            <input type="text" id="term" name="term" autocomplete="off" placeholder="foo bar baz">
          <label for="refineSelect" id="refine"><span>Refine term:</span><a href="#void" id="refa"> </a></label>
              <select id="refineSelect" name="refineSelect">
                <option value="foo">Foo</option>
                <option value="bar">Bar</option>
                <optgroup label="-- Try on --">
                  <option value="amazon">Amazon</option>
                  <option value="bing">Images - Bing</option>
                  <option value="google">Images - Google</option>
                  <option value="maps">Maps</option>
                  <option value="news">News</option>
                  <option value="wikipedia">Wikipedia</option>
                  <option value="youtube">YouTube</option>
                </optgroup>
                <optgroup label="-- Show all --">
                  <option value="baz">Baz</option>
                  <option value="quux">Quux</option>
                </optgroup>
              </select>

CSS:


#refineSelect {
  position: absolute;
  right: 0;
  top: 35px;
  z-index: 4;
}

#refineSelect optgroup, #refineSelect option {
  background-color: #fff;
}
        #refineSelect option:hover, #refineSelect option:focus {
          background-color: #ccc;
        }

Javascript:


(insde object Foo)
init: function () {
    var refineAnchor = document.getElementById('refa'),
         s = document.getElementById('refineSelect'),
         term = document.getElementById('term');

    Basis.addClass(s, 'hidden');
    s.size = s.length;

    //listens for the fake button to be clicked
    Basis.addEventListener(refineAnchor,'click',Foo.refineClickListener(s,term));
    Basis.addEventListener(s,'blur',Foo.sBlurListener);
...
    },

    showRefineSelect: function(s,term) {
        Basis.removeClass(s,'hidden');
        s.focus();

        //listen for keyboard
        s.onkeydown = (function(s,term) {
            return function(e) {
                var keyCode = (e.keyCode || e.which);
                //only do something when user hits ENTER
                if (keyCode && keyCode === 13) {
                        Foo.termParams(s,term);
                }
            }
        })(s,term);

       //listen for mouseclicks
        s.onclick = (function(s,term) {
            return function() {
                Foo.termParams(s,term);
            }
        })(s,term);
    },

    termParams: function(s, term) {
        term.value = term.value + ' ' + s.options[s.selectedIndex].value;
    },

    refineClickListener: function(s,term) {
        return function() {
            Foo.showRefineSelect(s,term);
        };
    },

    hideRefineSelect: function(s) {
        Basis.addClass(s, 'hidden');
    },

    sBlurListener: function(event) {
        Foo.hideRefineSelect(this);
    }
};

My biggest problem was that I needed
-the functions to run immediately
-the parameters s and term to get passed on to more functions
-the events so I could test for ENTER to prevent up/down arrows from causing the next function to run
(apparently in IE you can use CTRL plus up/down arrows to prevent this, but it’s little-known and IE-only anyway)

So, I was only getting the functions to run immediately with crap like
s.onchange = function() {
do stuff;
}
and this let me access the event, but I needed currying.

I had stuff like
s.onchange = function (params) {
return function() {
run stuff
}
}();

but the event kept getting lost.

Sadly, I had to wade through a lot of jQuery-based answers, trying to translate in my head to normal Javascript, where I saw somewhere that I could nest another function in that returned function who doesn’t have any params, thus not overwriting my (e). Arg.

Also all the times I tried currying and forgot the “run now” () at the end of the statement, arg.

Well so now anyway, now I has a public record of how I got a goofy UI with a forced-open select to work with both keyboard and mouse without the stupid arrow keys triggering “change” when they shouldn’t. It’s still not a smart design and not as accessible as I’d like, but, ffff. I’m downing a bottle of whisky anyways.

Is there any way this fails your specification?

<!DOCTYPE HTML>
<html>
<head>
<title>TEST</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
  <span id='result'>-</span><p>
  
  <input type='button' id = "sizer" value='Menu'><br>  
  
  <select id="refineSelect" name="refineSelect" style = 'display:none'>
    <option value="foo">Foo</option>
    <option value="bar">Bar</option>
    <optgroup label="-- Try on --">
      <option value="amazon">Amazon</option>
      <option value="bing">Images - Bing</option>
      <option value="google">Images - Google</option>
      <option value="maps">Maps</option>
      <option value="news">News</option>
      <option value="wikipedia">Wikipedia</option>
      <option value="youtube">YouTube</option>
    </optgroup>
    <optgroup label="-- Show all --">
      <option value="baz">Baz</option>
      <option value="quux">Quux</option>
    </optgroup>
 </select>
      
<script type="text/javascript">

(function()
{
    function action()
    { 
      if( sb.value != sb.lastValue )
      {
        document.getElementById( 'result' ).innerHTML = 'Something happened at ' + new Date().getTime(); 
        sb.style.display = 'none';
      }
        
      sb.lastValue = sb.value;  
    }
    
    function installHandler( obj, evt, func )
    {
      window.attachEvent ? obj.attachEvent(evt,func) : obj.addEventListener( evt.replace( /^on/i, "" ), func, false);
    }
    
    var sb = document.getElementById( "refineSelect" ),
        sizeBtn = document.getElementById( "sizer" );
        
    sb.lastValue = sb.value;    
    
    sb.size = sb.options.length + 2;
        
    installHandler( sb, 'onkeypress', function( e ){ if( ( e.keyCode || e.which )==13 ){ action(); }  } )
    
    installHandler( sb, 'onclick', action );
    
    installHandler( sizeBtn, 'onclick', function(){ sb.style.display = ( sb.style.display == 'block' ? 'none' : 'block' ); } );   

})();

</script>
</body>
</html>

Ali,

from the limited specification I gave in this thread, no, that pretty much doesn’t fail the specification (though there needs to be a blur handler, because there is more on this page and people may click elsewhere on the page or tab to the next item without choosing anything).

I hope some day to be able to write Javascript that concise. It’s clean.

I use onkeyup instead of onkeypress because of older Firefox fail: http://www.quirksmode.org/js/keys.html

I use a named object for namespacing esp since in this case, there will be other scripts added by someone (though they may also just remove my JS completely anyway). innerHTML is fast but I generally try to add stuff to the DOM the long way, and similarly, I rather add/remove classes than directly manipulate style properties (except when I’m testing/building), though that wasn’t your point I know.

With your IE/W3C event handlers, how do you deal with IE’s treatment of ‘this’ (for when it would be needed)? It would be good to know! Or do you generally try to avoid using ‘this’?

Why did you add 2 to the size? I noticed as well that the size shown isn’t the same as the size I state (but there’s always a scroll option).

One way is to pass the object as a parameter to the function creating the closure.

Why did you add 2 to the size? I noticed as well that the size shown isn’t the same as the size I state (but there’s always a scroll option).
Just to allow for the lines occupied by the optgroup labels.

slaps head the optgroups, lawlz…

What I’m using for IE’s treatment of ‘this’ is SitePoint’s Core library. It has quite a lot in it:


 if (document.attachEvent) {
        Core.addEventListener = function(target, type, listener) {
            if (Core._findListener(target, type, listener) != -1) {
                return;
            }
            var listener2 = function() {
                var event = window.event;

                if (Function.prototype.call) {
                    listener.call(target, event);
                }
                else {
                    target._currentListener = listener;
                    target._currentListener(event)
                    target._currentListener = null;
                }
            };
            target.attachEvent('on' + type, listener2);

            var listenerRecord = {
                target: target,
                type: type,
                listener: listener,
                listener2: listener2
            };
            var targetDocument = target.document || target;
            var targetWindow = targetDocument.parentWindow;
            var listenerId = 'l' + Core._listenerCounter++;

            if (!targetWindow._allListeners) {
                targetWindow._allListeners = {};
            }
            targetWindow._allListeners[listenerId] = listenerRecord;

            if (!target._listeners) { 
                target._listeners = [];
            }
            target._listeners[target._listeners.length] = listenerId;

            if (!targetWindow._unloadListenerAdded) {
                targetWindow._unloadListenerAdded = true;
                targetWindow.attachEvent('onunload', Core._removeAllListeners);
            }
        };
        Core.removeEventListener = function(target, type, listener) {
            var listenerIndex = Core._findListener(target, type, listener);
            if (listenerIndex == -1) {
                return;
            }

            var targetDocument = target.document || target;
            var targetWindow = targetDocument.parentWindow;

            var listenerId = target._listeners[listenerIndex];
            var listenerRecord = targetWindow._allListeners[listenerId];

            target.detachEvent('on' + type, listenerRecord.listener2);
            target._listeners.splice(listenerIndex, 1);

            delete targetWindow._allListeners[listenerId];
        };
        Core.preventDefault = function(event) {
            event.returnValue = false;
        };
        Core.stopPropagation = function(event) {
            event.cancelBubble = true;
        };
        Core._findListener = function(target, type, listener) {
            var listeners = target._listeners;
            if (!listeners) { 
                return -1;
            }

            var targetDocument = target.document || target;
            var targetWindow = targetDocument.parentWindow;

            for (var i = listeners.length - 1; i >= 0; i--) {
                var listenerId = listeners[i];
                var listenerRecord = targetWindow._allListeners[listenerId];

                if (listenerRecord.type == type && listenerRecord.listener == listener) {
                    return i;
                }
            }
            return -1;
        };
        Core._removeAllListeners = function() {
            var targetWindow = this;
            for (id in targetWindow._allListeners) {
                var listenerRecord = targetWindow._allListeners[id];
                listenerRecord.target.detachEvent('on' + listenerRecord.type, listenerRecord.listener2);
                delete targetWindow._allListeners[id];
            }
        };
        Core._listenerCounter = 0;
    }

which seems to take care a whole lot of stuff for me:
cross-browser attachEvent/addEventListener, finding the (window.)event and ‘this’/target, something to remove event listeners (for IE6 memory leak? not sure), and cross-browser event.returnValue=false/preventDefault and cancelBubble/stopPropagation.

The only ones I actually can read are the attachEvent-to-addEventListener and the preventDefault and stopPropagation functions. I can’t really follow all the target target2 listener2 -1 stuff. Meaning when I write something I can’t whip them out simply the way you did. You probably know exactly when you need these functions to work in IE.