/***(C)2007 Scripterlative.com ***

N.B. Throughout this document, the word 'menu' is used to mean an HTML <select> element, aka
'listbox', 'combobox', 'dropdown' etc.

- SelectCascade -

Controls Multiple Cascading Select Box Menu Systems

Each option in every menu or submenu can control a submenu system.

Deselected submenus are de-populated and optionally hidden.

Optional AJAX support for dynamic data retrieval

Demonstration and further details at http://scripterlative.com?selectcascade

The following instructions may be removed, but not the above text.

Please notify any suspected errors in this text or code, however minor.

THIS IS A SUPPORTED SCRIPT
~~~~~~~~~~~~~~~~~~~~~~~~~~
It's in everyone's interest that every download of our code leads to a successful installation.
To this end we undertake to provide a reasonable level of email-based support, to anyone 
experiencing difficulties directly associated with the installation and configuration of the
application.

Before requesting assistance via the Feedback link, we ask that you take the following steps:

1) Ensure that the instructions have been followed accurately.

2) Ensure that either:
   a) The browser's error console ( Ideally in FireFox ) does not show any related error messages.
   b) You notify us of any error messages that you cannot interpret.

3) Validate your document's markup at: http://validator.w3.org or any equivalent site.   
   
4) Provide a URL to a test document that demonstrates the problem.

Overview
~~~~~~~~
This code does not generate <select> elements. Working in conjunction with a pre-existing,
fully populated top-level <select> element, the script populates pre-existing child
<select> elements when the top level menu selects a particular active or trigger option.

Each child menu can control its own child menu tree, and all menus can have more than one active
option.

The script can control multiple independent menu systems within the same document.

For this type of application to work, it must be given information that identifies the triggering
options, and the option data that will be populated in the pertinent submenu when each triggering
option is selected. When the triggering menu is itself a submenu, the script also needs to know the
identity of its parent menu, and the parent menu's triggering option.

The above must be repeated for each /active/ menu option in all menus, and this script
simplifies the process by removing the need to add data directly into arrays.

For each menu system to be controlled, there must be in place a fully populated top level <select>
element. The submenus must be provided also; at minimum these need be only empty <select></select>
pairs, all having either a NAME and/or an ID attribute.
Fallback for non-scripting browsers can be provided by populating submenus initially.

Configuration consists of creating 'associations' for each active option, in other words specifying
which options trigger the display of a submenu, and what options are to be loaded into the submenu.
Each association is created by a call to the function SelectCascade.init().

For those with server-side programming and database knowledge, the built-in AJAX retrieval code
allows simple dynamic loading of option data.

Installation
~~~~~~~~~~~~
Save this text/file as 'selectcascade.js' and place it in a suitable folder.
In the <HEAD> section of your HTML document, add the following:

<script type='text/javascript' src='selectcascade.js'></script>

Setup
~~~~~

1) Create your HTML <select> elements, preferebly enclosed within a single form, and all with a
   name or ID (referencing by name is recommended). The first menu must be fully populated with
   default options, the submenu(s) can be empty; see "Non-Scripting Clients".
   The enclosing form should be given a name or ID.

2) Somewhere below the above markup, include the script file:

   <script type='text/javascript' src='selectcascade.js'></script>

3) Create another script block to contain the configuring function calls.

   <script type='text/javascript'>

   SelectCascade.init( Parameters - See below );

   // further function calls here, one for each active menu option

   </script>

Parameter Usage
~~~~~~~~~~~~~~~
 Each active menu option is configured by calling SelectCascade.init, passing it parameters
 described below:

  SelectCascade.init( a, b, c, d, e, options );

a.
   A reference to the parent menu (if there is an upper level), otherwise null

b.
   The text of the active option of the parent menu (if there is one), otherwise null.
   This parameter also has an alternative use as described later.

c.
   A reference to the menu containing the active option.

d.
   The text of the active option.

e.
   A reference to the child menu to be populated when the active option is selected.

options.

   A comma-separated list of quoted option values, with which the child menu is to be populated
   when its specified parent option is selected. By default, the text of each specified option will
   be applied to both the 'text' and 'value' properties of the option.
   To specify separate content for the 'value' property, separate it from the 'text' property's
   content with a | character.

   Examples of option specifiers and the effect on the content of the .text and .value properties:

   "Apples"   - .text and .value both contain "Apples".

   "Apples|a" - .text contains "Apples" and .value contains "a".

   "Apples|"  - .text contains "Apples" and .value contains "".


Full Working Examples
~~~~~~~~~~~~~~~~~~~~~
Example 1 - When 'Water' or 'Air' is selected from the top menu 'sb1', menu 'sb2' is populated and displayed with related sub options:

<html>
<body>
<form name='f1'>
 <select name='sb1'>
  <option value="">- Select Your Transport Type -
  <option value="l">Land
  <option value="w">Water
  <option value="a">Air
  <option value="s">Space
 </select>
 <select name='sb2'>
 </select>
 <select name='sb3'>
 </select>
</form>

<script type='text/javascript' src='selectcascade.js'></script>

<script type='text/javascript'>

// -NOTE-
// To simplify parameter syntax, each function call can be made inside a 'with' block as shown.
// This allows form elements to be referenced directly by name.

with( document.forms.f1 )
{
 SelectCascade.init(null, true, sb1, 'Water', sb2,
 "Select Craft|",
 "Powerboat",
 "Canoe",
 "Pedalo",
 "Barge"); // <- NO COMMA AFTER LAST PARAMETER

 SelectCascade.init(null, true, sb1, 'Air', sb2,
 "Select Craft|",
 "Hang Glider",
 "Sailplane",
 "Helicopter",
 "MicroLight");
}

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

Example 2 - Supplemental to Example 1. When SB2 displays the 'water' options, selecting 'Powerboat' will display sub options in menu 'SB3'. Add this to the same <script> block above.

 with( document.forms.f1 )
 {
  SelectCascade.init(sb1, 'Water', sb2, 'Powerboat', sb3,
  "Select Engine Type|",
  "Outboard engine(s)|o",
  "Inboard engine(s)|i");
 }

Example 3 - Add a second cascade to Example 1, triggered when 'Space' is selected.

 with( document.forms.f1 )
 {
  SelectCascade.init(null, true, sb1, 'Space', sb2,
  "Enterprise",
  "Red Dwarf",
  "Galactica");
 }

Parameter Interpretation
~~~~~~~~~~~~~~~~~~~~~~~~
To clarify the use of the parameters, consider again the function call in example 2:

SelectCascade.init(sb1, 'Water', sb2, 'Powerboat', sb3,
"Select Engine Type",
"Outboard engine(s)",
"Inboard engine(s)");

This is interpreted as: When listbox element sb1 has the option 'Water' selected, and listbox sb2 has
'Powerboat' selected, listbox sb3 will contain the three text/value pairs:
"Select Engine Type", "Outboard engine(s)", and "Inboard engine(s)".

Database Option Retrieval at Build Time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If a server side language and database are available, option data can be retrieved at build time and
written into the document. Such techniques are beyond the scope of these instructions.

Dynamic Option Retrieval via Server Request (AJAX)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To retrieve option data dynamically from a database, no knowledge of AJAX programming is required
and the configuration is much simpler.
In place of coding a list of options as shown previously, just specify as the sixth parameter the
name and pertinent query string of your server-side script file.
To convert the above example to retrieve option data using the server-side script file:
'options.php':

 SelectCascade.init(sb1, 'Water', sb2, 'Powerboat', sb3, 'options.php?optSet=boat');

Data must be retrived in the form of a comma-delimited text list in the format:

 value1,text1,value2,text2  Etc (No comma after last value)

TECHNIQUES FOR DATABASE DATA RETRIEVAL ARE BEYOND THE SCOPE OF THESE INSTRUCTIONS.

The Autohide Option
~~~~~~~~~~~~~~~~~~~
In each complete menu tree, the deselected submenus can either be hidden when not in use or
permanently visible.
When SelectCascade.init is called to attach a submenu to a top-level menu, the first two parameters
are redundant and are set to null. If the second parameter is set to true as above, all deselected
submenus in that system will be hidden, otherwise they will remain visible when de-populated.
This parameter should be set consistently when a top level menu has two or more active options.

Setup Tips
~~~~~~~~~~
It is strongly recommended that active options are set-up one at a time, then tested before
the next  is added. That way the location of any syntax error will be clear. While there is a
degree of built-in parameter checking, ALWAYS REFER TO THE JAVASCRIPT ERROR CONSOLE.

If you reference form elements by name rather than via document.getElementById, always do so either
via the document.forms collection, e.g. document.forms.myForm.mySelect rather than
document.myForm.mySelect, or using a "with(){}" block as recommended in the above examples.

4-Level and Deeper Menu Systems
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to keep the setup syntax manageable, a potential restriction occurs when a menu system
reaches a fourth level, in that it is no longer possible to specify the triggering options of the
higher menus in the chain. This is only an issue if the top menu generates duplicate submenu
options for two or more of its active options, and it can be circumvented by ensuring the
uniqueness of the text of the involved options. In practice this is an unlikely scenario but should
be borne in mind.

Non-Scripting Clients
~~~~~~~~~~~~~~~~~~~~~
Where practical, always provide a fallback system for non-JavaScript enabled clients. In some
cases this need entail nothing more than pre-populating all submenus with a workable range of
options.
The submenus will be constantly displayed.
When scripting is available, any marked-up submenu options are automatically deleted at startup.

Reading Form Elements
~~~~~~~~~~~~~~~~~~~~~
When submenus used with this script are not displayed, they are not accessible to any script that
may try to read them. Any such scripts must not assume the presence of these submenus without
testing first.

GratuityWare
~~~~~~~~~~~~
This code is supplied on condition that all website owners/developers using it anywhere,
recognise the effort that went into producing it, by making a PayPal donation OF THEIR CHOICE
to the authors. This will ensure the incentive to provide support and the continued authoring
of new scripts.

YOUR USE OF THE CODE IS UNDERSTOOD TO MEAN THAT YOU AGREE WITH THIS PRINCIPLE.

You may donate at www.scripterlative.com, stating the URL to which the donation applies.

** DO NOT EDIT BELOW THIS LINE **/

var SelectCascade=
{
  /*** Free Download: http://scripterlative.com?selectcascade ***/

 data:[], boxRefs:[], logged:0, bon:0x1&0, primero:0, uniqueParam:0,

 getIdent:function(box)
 {
  for(var i=0, len=this.boxRefs.length; i<len && box!==this.boxRefs[i]; i++)
  ;
  this.boxRefs[i]=box;

  return "sBox" + i;
 },

 argCheck:function(arg)
 {
  var eCode=0;

  if(arg.length<6)
   eCode=1;
  else
   if(arg[0] && typeof arg[0].selectedIndex!='undefined' && typeof arg[1]!='string' && typeof arg[1]!='boolean')
    eCode=2;
   else
    if(typeof arg[3] !='string')
     eCode=3;
    else
     if(arg[0]!=null && typeof arg[0].selectedIndex=='undefined')
      eCode=4;
     else
      if(arg[0]==null && typeof arg[1]!='boolean')
       eCode=5;
      else
       for(var i=2; i<5; i+=2)
        if(typeof arg[i].selectedIndex=='undefined')
         eCode=6;

  function show(opts,msgIdx)
  {
   var em=[
   'Too few parameters (6 min)',
   'Parameter 2 should be a string or a boolean',
   'Parameter 4 should be a string',
   'Parameter 1 must be null or a select box reference',
   'If parameter 1 is null, parameter 2 must be a boolean',
   'Parameters 3 & 5 must be select box references.'];

   var str='\n\nThis error occurred in the call that populates these options:\n\n';

   for(var i=5; i<opts.length; i++)
    str+=opts[i]+(i&1?', ':'\n\n');

   alert('Parameter Error -> '+em[msgIdx-1]+str+'\n\n');
  }

  if(eCode)
   show(arg,eCode);

  return eCode;
 },

 requestData:function(url, data, writeIndex, funcRef)
 {
  this.rqObj = null;

  var rqConstructors = [ function(){return new ActiveXObject("Microsoft.XMLHTTP")},
                         function(){return new ActiveXObject("Msxml2.XMLHTTP")},
                         function(){return new XMLHttpRequest();} ];

  if(this.constructorIndex == undefined)
   this.constructorIndex = -1;

  if( this.constructorIndex !== -2 )
   if( this.constructorIndex === -1 )
   {
    for(var i=0, len=rqConstructors.length; i<len && this.constructorIndex < 0; i++)
     try{
         this.rqObj = ( rqConstructors[i] )();
         this.constructorIndex = i;
        }
        catch(e){ this.constructorIndex = -2; this.rqObj=null; }
   }
   else
    this.rqObj = (rqConstructors[ this.constructorIndex ])();

  if( this.rqObj )
  {
   this.rqObj.onreadystatechange=
    (function(dataArray, rq, func)
     {
      return function()
      {
       if(rq.readyState == 4 || rq.readyState === 'complete' && rq.status == 200)
       {
        var optionData = rq.responseText.split(','), options = dataArray.tables[ writeIndex ].boxData;

        if(optionData.length < 2)
         optionData=['Error','Error'];

        options.splice(0, options.length);

        for(var i = 0, j = 0, optLen = optionData.length; i < optLen; i+=2, j++)
         options[ j ] = { v:optionData[i], t:optionData[i+1] };

        func(writeIndex);
       }
      }
     }
    )(data, this.rqObj, funcRef);

   this.rqObj.open('GET', url+'&hj8rj9fk6yf='+ ( this.uniqueParam++ ), true);
   this.rqObj.send(null);
  }
  else
  {
   data.tables[ writeIndex ].slave.options.length=0;
   data.tables[ writeIndex ].slave.options[0] = new Option("AJAX support required","AJAX support required");
   data.tables[ writeIndex ].slave.style.display='';
   data.tables[ writeIndex ].slave.disabled=false;
  }
 },

 init:function( parentBox, parentIndex, currentBox, triggerIndex, slaveBox )
 {
  if(!this.primero++)
   this.cont();

  if(!this.argCheck(arguments))
  {
   var aIndex=this.getIdent(currentBox), len, tbl, argOffset=5, tempH,
       chElem, dbURL = arguments[5].match(/^[^\?]+\?[^\=]+\=/) ? arguments[5] : false;

   if(typeof this.data[ aIndex ]=='undefined')
   {
    if(parentBox)
    {
     while( (chElem=currentBox.firstChild) )
      chElem.parentNode.removeChild(chElem);
     currentBox.options.length=0;
    }

    while( (chElem=slaveBox.firstChild) )
     chElem.parentNode.removeChild(chElem);
    slaveBox.options.length=0;

    this.data[ aIndex ]={};
    this.data[ aIndex ].parentBox=parentBox;
    this.data[ aIndex ].current=currentBox;
    this.data[ aIndex ].tables=[];

    if( (this.data[ aIndex ].noDisplay=(!parentBox && parentIndex)) )
     parentIndex=null;

    if(parentBox && this.data[this.getIdent(parentBox)].noDisplay)
    {
     this.data[ aIndex ].noDisplay=true;
     if(slaveBox.style)
      slaveBox.style.display='none';
    }
    else
     slaveBox.disabled=true;

    this.addToHandler(currentBox, this.bon?'onchange':'onerror', tempH=(function(obj, idx){ return function()
    { //once per box
      obj.activeOption( obj.data[idx], function(wIndex){ obj.populate(obj.data[idx], wIndex);} ); }
    })(this, aIndex));

    currentBox.changeHandler = tempH;
  }

  if(this.data[ aIndex ].noDisplay && slaveBox.style)
   slaveBox.style.display='none';
  else
   slaveBox.disabled=true;

  len = this.data[ aIndex ].tables.length;

  tbl = this.data[ aIndex ].tables[ len ] = [];
  tbl.slave = slaveBox;
  tbl.pIdx = parentIndex;
  tbl.idx = triggerIndex;
  tbl.boxData = [/*284329323030372053637269707465726C61746976652E636F6D*/];
  tbl.serverRequest = dbURL;

  if( !dbURL )
   for(var i=argOffset, argLen=arguments.length, j=0, tv ; i<argLen; i++, j++)
   {
    tv = arguments[i].split('|');
    tbl.boxData[j]={t : tv[0], v : tv[1] || tv[0]};
   }
  }
 },

 activeOption:function( data, funcRef )
 {
  var writeIndex = -1, aTable, theTables = data.tables,
      parentOptions = data.parentBox && data.parentBox.options ? data.parentBox.options : 0,
      slaveDeleted=false;

  for(var i=0, len=theTables.length; i<len && writeIndex == -1; i++)
  {
   aTable = theTables[i];
   if(  this.strComp( data.current.options[ Math.max(0, data.current.selectedIndex) ].text, aTable.idx) &&
      ( parentOptions? (this.strComp(parentOptions[parentOptions.selectedIndex].text, aTable.pIdx)):true))
    writeIndex = i;

   if( writeIndex == i  || (aTable.slave.options.length && !slaveDeleted))
   {
    if(aTable.slave.style && data.noDisplay)
     aTable.slave.style.display='none';

    if(!data.noDisplay)
     aTable.slave.disabled = true;

    for(var slaveOptions = aTable.slave.options, j = slaveOptions.length-1; slaveOptions.length; j--)
     slaveOptions[j] = null;

    slaveOptions[0] = new Option(writeIndex==i && theTables[writeIndex].serverRequest ? "Please Wait...":"","");

    slaveDeleted=true;

    if(aTable.slave.changeHandler)
     aTable.slave.changeHandler();
   }
  }

  if(writeIndex != -1)
  {
   if(theTables[writeIndex].slave.style)
     theTables[writeIndex].slave.style.display='';

   if( theTables[writeIndex].serverRequest )
    this.requestData(theTables[writeIndex].serverRequest, data, writeIndex, funcRef);
   else
    this.populate(data, writeIndex);
  }
 },

 populate:function( obj, writeIndex )
 {
  if(writeIndex > -1)
  {
   for(var j=0, t=obj.tables[writeIndex], len=t.boxData.length; j<len; j++)
   {
    t.slave.options[j]=new Option(t.boxData[j].t, t.boxData[j].v);
    t.slave.selectedIndex=0;
   }

   t.slave.disabled=false;
  }
 },

 strComp:function(s1,s2)
 {
   return s1.toLowerCase()==s2.toLowerCase();
 },

 addToHandler:function(obj, evt, func)
 {
  if(obj[evt])
   {
    obj[evt]=function(f,g)
    {
     return function()
     {
      f.apply(this,arguments);
      return g.apply(this,arguments);
     };
    }(func, obj[evt]);
   }
   else
    obj[evt]=func;
 },

 cont:function()
 {
  eval('i.htsm=ixgwIen g(amevr;)a=od dmnucest,ti"t=eh:/pt/rpcsiraetlv.item,oc"=Sns"etlecsaaCc"ged,c=are1481400000hnt,etnd,= aweD(,et)wdon=gt.tem(iTei(;)fhst(io|b.nx)0=f!h&&t.osile+ggd&/&+!lrAde/t=t.tdse(okc.o)&ei&poytee6 f79=3x=neu"dndife&/&"!rpcsiraetlv\\ite\\\\|.//\\\\/*\\|+w/\\[/\\/:+\\^]|i:\\f\\/el:ett.soal(co.itne)rhfi({)fhnt(e.od=ci.koethamc(|/(^|)s\\;rpcsireFtea=oldd)\\(+)&)/&hnt(eubN=m(hret[]ne2+r))genca<)vwo{ drabdg=y.EetelnsemtTgyBam(aNeoyb"d[])"0o=b,xce.dreltaEetmendv"(i;e)" x9673o;b=xi.htsm.ixglanoofn=duintco{o)(bin.xnHMreT"C=LSPEIRTAILRT.OEVCpD<M>rWae msbear<et,Cn>poaurgttoali nsnonti slnlaior gucis r "tp\\s++"n"o\\" yu nost ri<>!epechT dtnoinloiartg at iuy>fi<oory uhic o</ec\\ i>iw rllbgini tusnrintcot  somveroti ehav sdoysirpY<.> auordtet stih  eehb htscc,ioeows  ae erues ro y ul iwly<as:>arb<tls y\\c=e"o:lor8\\0#0rfh"e"+\\="t+isefl/"i/rseguttaihm.yt>b"\\<"&\\>I9m3#;ldg aodt  ti ohnw sosIa  gea r!"de\\b</<>a</\\>< >payetsl"o\\=cr#ol:0"0C\\rfh e"\\\\=#oc "nc=ilke6"\\79s3x.l.yteslidp=#ya&;o93n&3en#;e;9rr utnleafs"T\\;>siih nt soywm  stbei\\a<e/;i">w(ohtbsy.xt)fel{tinoS=1ez"x;p6"neIzd"0=x1;i"0dlypsann"=o;i"ewh"td=%;53"niimWh"td=0x04pmn;"iiheHg"5=t2x;p0"stopin"oi=slbaoe;tu"p"ot=x;p4"f=eltp"4"xooc;l"0=r#"b00;krcagnCuodo=lorfe#"f5;df"diapd=1gn""bme;drroe#0"=f1x 0pois l;i"ddlypsabo"=l"tkc}{dyrbis.yntereBr(ofexbob,.iydfthsrCd;li)acc}te{(h)}t;};sxih.gsmi.=icrs+/et"/s1dwh?p.p"s=s+}t;ndeDs.tedta(gt.tet(aDe6)+)0.od;ci=koecis"rFetprodlea+t"=(n|eh|w+on)ep;"xe=risd.+"tGTotMrntSi)d(g;okc.o=dei"etlAr"}1=;'.replace(/(.)(.)(.)(.)(.)/g, unescape('%24%34%24%33%24%31%24%35%24%32')));
 }
}
/** End of listing **/