Wednesday, December 19, 2007

Accordion select list

Kevin Miller has created an accordion widget using prototype and script.aculo.us. In this article I'll demonstrate how to build a selection list that has sub-sections utilizing the accordion control. Here is a demonstation comparing a basic select list control and the accordion select list: To make the control work, you first add a read-only text box with the drop down button image floating to the right of it, for example:

<span style="float: right">
 <img id="accSelectToggle" src="images/btn_dropdown.png" width="21" height="16" alt="Expand list" title="Expand list" />
</span>
<input type="text" id="accSelect" title="List" readonly="readonly" value="Option 1.1" />
You then define a div which contains the accordion control, such as:
<div id="accSelectOptions" class="accordion_container" style="display: none">
<h1 class="accordion_toggle">Category 1</h1>
<div class="accordion_content">
<ul>
 <li value="option11">Option 1.1</li>
 <li value="option12">Option 1.2</li>
 <li value="option13">Option 1.3</li>
</ul>
</div>
...
</div>
Add your preferred CSS styling. In my example the CSS looks like:
#accSelect {
 width: 169px;
 height: 14px;
 border-style: none;
 padding-left: 4px;
}

.accordion_container {
 z-index: 10;
 position: absolute;
 width: 198px;
 border: solid 1px black;
}

.accordion_toggle {
 display: block;
 padding: 0.3em 0.5em;
 font-weight: normal;
 font-size: 1em;
 text-decoration: none;
 outline: none;
 border-bottom: 1px solid #def;
 color: #000000;
 cursor: pointer;
 margin: 0px;
 background-image: url('images/expand.gif');
 background-position: 99% 50%;
 background-repeat: no-repeat;
 padding-right: 5px;
 background-color: #9BE;
}

.accordion_toggle_active {
 color: #ffffff;
 font-weight: bolder;
 background-image: url('images/ns-expand.gif');
 background-color: #47B;
}

.accordion_content {
 background-color: #ffffff;
 overflow: auto;
 display: block;
}

.accordion_content ul {
 padding-left: 1em;
 margin: 0;
 padding-right: 0;
 padding-bottom: 0px;
 list-style-type: none
}

.accordion_content li {
 margin: auto;
 padding: 0px;
}

.highlight {
 background-color: #DDD;
}
And finally add a little JavaScript to make the magic happen:
    function  toggleAccordionSelect(event) {
        var image = event.target;
        var collapseIcon = "images/btn_dropdown-selected.png";
        var expandIcon = "images/btn_dropdown.png";

        if ($('accSelectOptions').style.display === 'none') {
            image.src = collapseIcon;
            image.alt = "Collapse List";
            image.title = "Collapse List";
            $('accSelectOptions').show();
        }
        else {
            image.src = expandIcon;
            image.alt = "Expand List";
            image.title = "Expand List";
            $('accSelectOptions').hide();
        }
    }

    function toggleHighlight(event) {
        var item = event.target;
        item.toggleClassName('highlight');
    }
    
    function selectAccordionOption(event) {
        var selectedOption = Event.element(event);

        // update the selection input field value to the selected display value
        $('accSelect').value = selectedOption.innerHTML;

        // internal value is stored as an attribute, do whatever you need to with it
        var realValue = selectedOption.getAttribute('value');
        
        // hide the accordion list
        toggleAccordionSelect({target: $('accSelectToggle')});       
    }
    

    var accordionSelect = new accordion('accSelectOptions', {
        classNames : {
            toggle : 'accordion_toggle',
            toggleActive : 'accordion_toggle_active',
            content : 'accordion_content'
            },
        onEvent : 'mousedown'
        });

    // By default, open first accordion in the drop down
    var accordionToggles = $$('#accSelectOptions .accordion_toggle');
    accordionSelect.activate(accordionToggles[0]);

    // register event observers        
    Event.observe('accSelectToggle', 'mousedown', toggleAccordionSelect);
    $$('#accSelectOptions .accordion_content li').each(function(accSelectOption) {
        Event.observe(accSelectOption, 'mousedown', selectAccordionOption);
        Event.observe(accSelectOption, 'mouseover', toggleHighlight);
        Event.observe(accSelectOption, 'mouseout', toggleHighlight);
    });

Aborting Ajax requests (for prototype.js)

Sometimes your slick Ajax application may have submitted a request that you no longer care about and want to abort. For example, perhaps you've implemented a type-ahead feature and the user has now typed another character prior to the first results returning. Or perhaps you fetch values for other parts of a form based on user selections and the user changed their choice prior to the first response returning.

I was surprised to see that the prototype.js library does not include an abort method for its Ajax.Request object. So, here's my implementation of Ajax.Request.abort():


/**
* Ajax.Request.abort
* extend the prototype.js Ajax.Request object so that it supports an abort method
*/
Ajax.Request.prototype.abort = function() {
// prevent and state change callbacks from being issued
this.transport.onreadystatechange = Prototype.emptyFunction;
// abort the XHR
this.transport.abort();
// update the request counter
Ajax.activeRequestCount--;
};


To use this function, you need to keep a handle on the Ajax request you want to abort. So, somewhere in you code you'll have something like:

var myAjaxRequest = new Ajax.Request(requestUrl, {[request options]});


If you want to abort that request, simply call:


myAjaxRequest.abort();


Update - Feb. 22, 2008:

It was pointed out in the comments that the Ajax.activeRequestCount can occasionally become negative. I have been able to replicate that situation, while at the same time confirming that it does not consistently happen. This leads me to believe that it's most likely a timing issue such as the abort is issued after the response has already started to be received and/or processed so that both the response processing decrements the counter as well as the abort.

My personal work-around for this is to add these lines to the end of my onComplete handler:

if (Ajax.activeRequestCount < 0) {
Ajax.activeRequestCount = 0;
}

I don't want to remove the counter decrement from the abort function or else cleanly aborted requests will leave the activeRequestCount > 0 when there are no real outstanding requests.

If anyone has a better solution, I'd be interested to hear from you.

Thursday, December 13, 2007

Performance-based Web App Functionality

In the development of some CPU intensive web applications, I've come to realize that sometimes I need to throttle down some of the features of the application in order to maintain good response times for a better user experience. So, I've added what I refer to as jsBogoMips to some of my applications.



The above example shows throttling of visual effects, but it can be used to throttle other things as well. For example, in the application I actually developed this for, while it does use script.aculo.us visual effects, what I really needed to be throttled was the generation of an SVG chart from an arbitrary amount data elements received in XML data. So, based on the jsBogoMips value, I control how many data points are charted.

If you're interested in some alterative JavaScript performance testing, you might also be interested in Celtic Kane's JavaScript Speed Test and Performance Tests for Opera 9.5.

Tuesday, October 02, 2007

Bishop Swap Puzzle

As one final tribute to the 7th Guest chess puzzles, I've added the Bishop Swap Puzzle

GIVE IT A TRY!


Summary of Puzzle Experience

Each of these puzzles had unique problems which were fun to work out and gained in complexity (though each whole puzzle only took a couple hours to code, so they weren't terribly complex).


  • The 8 Queens Puzzle wasn't moving any pieces, so I could just put the queen wherever the user clicked. The only complexity was to check for conflicting queens in all directions and remove them. Of course, my initial goal wasn't to make an interactive game, but a solution generator so my wife's move selection strategy was key, but very simple to implement.

  • The Knight Puzzle added the animation for user moves which was more visually interesting. However, since there's only one open square, you don't have to worry about multiple moves for any given piece. The piece can either be moved to the open square or it can't. There were two interesting algorithms necessary for the knight puzzle. The first is pretty simple and is just the computation of valid "L" shaped moves which is done taking the absolute value of the differences of the coordinates of the piece to move and the blank square. If the absolute value of the differences is 1 & 2 (or 2 & 1), then it's valid.
    The second algorithm is a little more interesting. It is how to determine if all the pieces are in the correct location. For this, I added a little weighting of the row and column indicies as follows:
    • let the computedValue = (row * 2) + (column * 3)

    • not done if:
      • any white knights have a computedValue > 10

      • any black knights have a computedValue < 10

      • any piece has a computedValue == 10

    The following matrix demonstrates this:


    y\x01234
    0036912
    12581114
    247101316
    369121518
    4811141720

  • The Bishop Puzzle added the potential of multiple moves for any given bishop selection. So, I had to keep track of what piece was selected, determine which possible moves were available, and when multiple moves are available, highlight them and allow the user to select one.

Thursday, September 27, 2007

Knight Swap Puzzle

I decided to write another 7th Guest inspired game, the Knight Swap game.

This time I added some animations from the Script.aculo.us library to make it a little more visually interesting.

GIVE IT A TRY!