TECHNICAL RECIPES LIMITED

How to write a Facebook-style multi select javascript control - Part 1





Intro

Welcome to my first article on writing javascript controls. For this article I'll be taking the reader through the basics of creating a Facebook style multi selection listbox. Part 1 sets up a basic dropdown using divs and a input text.

Objectives

We aim to achieve the following:

  • Create a facebook style javascript multi selection listbox...
  • that works with IE, Firefox and Chrome,
  • that's easy to integrate with Ajax,
  • to make it both a jQuery plugin and a standalone javascript,
  • and that's independent of server-side platforms including php and dotnet/aspnet.

We start with a javascript array of string values.


<script type="text/javascript">
    var aList = ["One", "Two", "three", "Four", "Five"];
</script>

Next we need a textbox and a div list and some javascript functions to hide and show the div when the textbox gains/loses focus or the enter key is hit while the textbox is focused.

<script type="text/javascript">
// Common convention for retrieving by Id 
// Delete if using another javascipt lib with $(). 
$ = function(s){return document.getElementById(s);}; 

function procKey(e){ 
	var d=$('d'); 	
	
	// Enter key 
	if(e.keyCode=='13') 
	{ 
		if (d.style.display=='none') d.style.display=''; 
		else 
		{ 
			d.style.display='none'; 
		} 
		return 0; 
	}
}

</script>

<input type="text" id="txt1" onkeyup="return procKey(event);" />
<div id="d" 
  style="display:none;width:200px;border:solid 1px silver;"></div>

Generating the list div

To make it easier to add to a page, we really only want to place a textbox and have the javascript control generate the rest.


var d = document.createElement("div");
$("txt1").parentNode.insertBefore(d, txt1.nextSibling);

Packaging it into an object
Things are going to start getting complicated so let's organise things into javascript classes. We will have a class for the textbox, one for the Main div list and one for the list item divs. We want our actual textbox and divs to be of those classes rather than having an associated class. We do this using the Class.call(obj) method which is the same as new Class() but it makes an existing object inherit from that class...in a makshift way.


<script type="text/javascript"> 

// declare a Class for the the textbox called acText
function acText()
{
    // apply the class members to "this" and do initialization
    
    // create div
    this.d = document.createElement("div");
    this.d.style.display="none";
    this.parentNode.insertBefore(d, txt1.nextSibling);
    
    // change the onfocus event handler not that it's initialized
    this.onfocus = function() {
                // show div
                this.d.style.display="";
                };
               
}

// Then make txt1 inherit from it in the onfocus

</script> 

<input type="text" id="txt1" onfocus="acText.call(this);" />

Now let's add the arrow key processing and use enter as a selection.  We also add in an Init() function which sets everything in place when the textbox is first given focus.  It creates the div list from the array and sets events.

Note the use of setTimeout for the blur event. When an item is clicked, the textbox loses focus and it then given back focus immediately. The setTimeout gives a little delay to allow this to happen.

 

var aList = ["One", "Two", "Three", "Four", "Five"];

    $ = function(s) { return document.getElementById(s); };

    function acListItem(t, d) {
        this.t = t;
        this.d = d;

        this.onclick = function() {
          var d = this.d;
          if (d.style.display == 'none') d.style.display = '';
          else {
            this.t.value = this.innerHTML;
            this.t.quietFocus();
            d.style.display = 'none';
          }
          d.setSelectedCell(this);

          return;
        }

        this.onmouseover = function() { this.className = this.className.replace("clHighlight", ""); this.className += " clHighlight"; };
        this.onmouseout = function() { this.className = this.className.replace(" clHighlight", ""); };

        return this;
    };

    function acList(t, lst) {
        this.t = t;

        for (i in lst) {
            var newItem = document.createElement("div");
            newItem.className = 'noselDiv';
            newItem.innerHTML = lst[i];
            this.appendChild(newItem);
            acListItem.call(newItem, t, this);
        }
        this.setSelectedCell = function(c1) {
          var c = this.firstChild;
          while (c != null) {
            if (c == c1) c.className = 'selDiv';
            else c.className = 'noselDiv';
            c = c.nextSibling; i++;
          }
        };
        this.hide = function() { this.style.display='none'; };
        this.show = function() { this.style.display=''; };

    };

    function acText(lst) {
        var t = this;
        var d = document.createElement("div");
        
        this.parentNode.insertBefore(d, t.nextSibling);
        this.d = d;
        this.d.className="dd";
        d.id = "d_" + this.id;
        this.quietFocus = function() { var fe = this.onfocus; this.onfocus = null; this.focus(); this.onfocus = fe; };
        this.onfocus = function() { setTimeout(function() { this.value += "."; d.show(); }, 100); };
        this.onclick = function() { if (this.d.style.display == '') this.d.hide(); else this.d.show(); };
        this.onblur = function() { setTimeout(function() { if (!t.focused) t.d.hide(); }, 250); };
        this.onkeyup = function(e) {
            if (!e) e = event;
            var d = this.d;
            var currentIndex = -1;
            var c = d.firstChild;
            var i = 0;
            while (c != null) {
                if (c.className == 'selDiv') currentIndex = i;
                c = c.nextSibling;
                i++;
            }
            //uparrow
            if (e.keyCode == '38') {

                if (d.style.display == 'none') {
                    d.show();
                    return;
                }
                else {

                    if (currentIndex >= 1) currentIndex--;
                    else currentIndex = d.childNodes.length - 1;

                    var i = 0;
                    var c = d.firstChild;
                    while (c != null) {
                        if (i == currentIndex) c.className = 'selDiv';
                        else c.className = 'noselDiv';
                        c = c.nextSibling; i++;
                    }
                }
                return 0;

            }
            //downarrow
            if (e.keyCode == '40') {
                if (d.style.display == 'none') {
                    d.show();
                    return;
                }
                else {

                    if (currentIndex < d.childNodes.length - 1) currentIndex++;
                    else currentIndex = 0;

                    var i = 0;
                    var c = d.firstChild;
                    while (c != null) {
                        if (i == currentIndex) c.className = 'selDiv';
                        else c.className = 'noselDiv';
                        c = c.nextSibling; i++;
                    }

                }
                return 0;

            }
            // enter key
            if (e.keyCode == '13') {
                if (d.style.display == 'none') d.show();
                else {
                    if (currentIndex > -1)
                        this.value = d.childNodes[currentIndex].innerHTML;
                    d.hide();
                }
                // return false;
            }


            // return;
        };

        acList.call(d, this, lst);
        d.hide();

    };   
    
Try it out

Next: Autocomplete/Filtering dropdown

A couple more things could be tidied up with this dropdown div.
- If the textbox is at the bottom of the screen, we need to pop it up above rather than below.
- If one of the items matches the text in the textbox, it should be shown as selected.