  // $Id: cascadingSelect.js,v 1.10 2009/09/17 12:30:52 alan Exp $

  // ----- Implementation Notes ---------------------------------------------
  //
  // - JavaScript 1.2 does not support Array.push()
  // - Using undefined global variables in class methods causes errors in IE5
  //

  // ----- MenuItem ---------------------------------------------------------

  function MenuItem_getOption( classId, selectedFlag ) {
    return new Option( this.label, classId + "@" + this.levelCode, false, selectedFlag );
  }

  function MenuItem( valueId, dataTypeId, fromToFlag, levelCode, label, subMenu ) {

    // -- Attributes

    this.valueId    = valueId;
    this.levelCode  = levelCode;
    this.dataTypeId = dataTypeId;
    this.fromToFlag = fromToFlag;
    this.label      = label;   	// text displayed in select
    this.subMenu    = subMenu; // a Menu object

    // -- Methods

    this.getOption    = MenuItem_getOption;
    this.getValueId   = function() { return this.valueId;   };
    this.getLevelCode = function() { return this.levelCode; };
    this.getSubMenu   = function() { return this.subMenu;   };

  }

  // ----- Menu -------------------------------------------------------------

  function Menu_addItem( valueId, dataTypeId, fromToFlag, levelCode, code, label, subMenu ) {
    this.items[ this.items.length ] = new MenuItem( valueId, dataTypeId, fromToFlag, levelCode, label, subMenu );
    this.length++;
    if ( subMenu ) {
      subMenu.setParent( this, levelCode );
    }
    this.selectSet.addCode( this, valueId, levelCode, code );
  }

  function Menu_getItem( levelCode ) {
    for( var i = 0; i < this.length; i++ ) {
      var item = this.items[i];
      if ( item.levelCode == levelCode ) { 
        return item; 
      }
    }
    return null;
  }

  function Menu_setSelected( levelCode ) { 
    var fieldName = this.select.fieldName;
    var selectSet = this.select.parent;
    var formName = selectSet.getFormName();

    // -- Setup the select menu

    var element = document[ formName ][ fieldName ];
    element.options.length = 0;
    element.style.display='';  
    element.options[ 0 ] = new Option( selectSet.getUnselectedText(), this.classId + "@-1" );
    var selectedItem = null;
    for( var i = 0; i < this.length; i++ ) {
      var v = this.items[i].getLevelCode();
      var selectedFlag = ( v == levelCode );
      element.options[ i+1 ] = this.items[i].getOption( this.classId, selectedFlag );
      if ( v == levelCode ) {
        selectedItem = this.items[i];
      }
    }

    // -- Display from/to fields if necessary

    //alert( "Menu_setSelected: levelCode: " + levelCode ); // AHAHAH
    this.getSelect().showFromToFields( selectedItem );

    // -- Set the next menu (the one to the left)

    if ( this.parentLevelCode ) {
      var menu = selectSet.getMenuByLevelCode( this.parentLevelCode );
      menu.setSelected( this.parentLevelCode );
    }
  }


  function Menu_getParentClassId() {
    var parent = this.getParent();
    return parent ? parent.classId : null;
  }

  function Menu_init( ) {
    var fieldName = this.select.fieldName;
    var selectSet = this.select.parent;
    var formName = selectSet.getFormName();

    var element = document[ formName ][ fieldName ];
    element.options.length = 0;
    element.style.display='';  

    if ( this.length == 0 ) { // Empty menu gets the 'unselected text'
      element.options[ 0 ] = new Option( selectSet.getUnselectedText(), -1 );
      element.style.display='none';  
    } else { // Multi-item menu gets an 'unselected text' entry prepended
      element.options[ 0 ] = new Option( selectSet.getUnselectedText(), this.classId + "@-1" );
      for( var i = 0; i < this.length; i++ ) {
        element.options[ i+1 ] = this.items[i].getOption( this.classId );
      }
      this.select.disableNext();
    }
  }

  function Menu( classId, selectSet, level ) {

    // -- Attributes

    // -- Constructor

    this.classId   = classId;
    this.selectSet = selectSet;		// SelectSet object that this menu belongs to
    if ( selectSet == null ) {
      alert( 'ERROR: Menu: selectSet is null!' );
      return;
    }
    this.select   = selectSet.get( level );
    if ( this.select == null ) {
      alert( 'ERROR: Menu select is null' );
    }
    this.items    = Array();		// Array of MenuItem ojects that belong to this menu
    this.length   = 0;			// Number of 
    this.parent   = null;		// Parent Menu object
    this.parentLevelCode = null;	// Parent Menu's levelCode

    this.select.addMenu( this );

    // -- Methods

    this.addItem      = Menu_addItem;
    this.getItem      = Menu_getItem;
    this.getFirstItem = function() { return this.items[0]; }
    this.setParent    = function( parent, levelCode ) { this.parent = parent; this.parentLevelCode = levelCode; }
    this.getParentClassId = Menu_getParentClassId;
    this.getParent    = function() { return this.parent; }
    this.getSelect    = function() { return this.select; }
    this.setSelected  = Menu_setSelected;
    this.init         = Menu_init;
  }

  // ----- Select -----------------------------------------------------------

  function Select_disableNext() {
    if ( this.next ) {
      var next = this.next;
      next.hide();
      this.next.disableNext(); // ..recurusively disable the rest
    }
  }

  function Select_addMenu( menu ) { 
    this.menus[ this.menus.length ] = menu; 
    this.parent.addMenu( menu );
  }

  function Select_show() {
    var fieldName = this.select.fieldName;
    var formName  = this.select.parent.getFormName();

    var element = document[ formName ][ fieldName ];
    element.style.display='';  
  }

  function Select_hide() {
    var fieldName   = this.fieldName;
    var formName    = this.parent.getFormName();
    var element     = document[ formName ][ fieldName ];

    element.options.length = 0;  
    element.style.display='none';  
    element.options[ 0 ] = new Option( this.parent.getUnselectedText(), -1 );
    this.hideFromToFields();
  }

  function Select_showFromToFields( item ) { // item is a MenuItem
    var frDisplayFlag = false;
    var toDisplayFlag = false;

    //alert( "Menu_showFromToFields: item = " + ( item == null ? "NULL" : "not NULL" ) );

    if ( ! item ) { return; }

    switch( item.dataTypeId ) {
    case '':
    case 0: // None?
      frDisplayFlag = false;
      break;

    case 2: // Integer
    case 3: // Float
    case 4: // Date
      frDisplayFlag = true;
      break;
  
    case 1: // Boolean
    case 5: // Text
    case 6: // URI
    case 7: // None
      alert( "Menu_showFromToFields: ERROR: unimplemented dataTypeId: " + item.dataTypeId );
      frDisplayFlag = false;
      break;
    
    default:
      alert( "Menu_showFromToFields: ERROR: unknown dataTypeId: " + item.dataTypeId );
      frDisplayFlag = false;
      break;
    }

    if ( item.fromToFlag && frDisplayFlag ) {
      toDisplayFlag = true;
    }

    var formName  = this.parent.getFormName();

    //alert( "Menu_showFromToFields: " + "\n" +
    //       "  frDisplayFlag: " + ( frDisplayFlag ? 'SHOW' : 'HIDE' ) + ", field: " + this.frFieldName + "\n" +
    //       "  toDisplayFlag: " + ( toDisplayFlag ? 'SHOW' : 'HIDE' ) + ", field: " + this.toFieldName 
    //     );

    // -- Set visibility of the from/to fields

    document[ formName ][ this.frFieldName ].style.display    = ( frDisplayFlag ? 'inline' : 'none' );  
    document[ formName ][ this.toFieldName ].style.display    = ( toDisplayFlag ? 'inline' : 'none' );  
    document.getElementById( this.toFieldName ).style.display = ( toDisplayFlag ? 'inline' : 'none' );  
  }

  function Select_hideFromToFields() {
    var formName    = this.parent.getFormName();
    var frFieldName = this.frFieldName;
    var toFieldName = this.toFieldName;

    document[ formName ][ frFieldName ].style.display    = 'none';
    document[ formName ][ toFieldName ].style.display    = 'none';
    document.getElementById( toFieldName ).style.display = 'none';
  }

  function Select( fieldName, frFieldName, toFieldName, parent ) {

    // -- Attributes

    this.fieldName   = fieldName;   // Name of field where levelCode is stored (String)
    this.frFieldName = frFieldName; // Name of 'from field' text entry field
    this.toFieldName = toFieldName; // Name of 'to field' text entry field

    this.fieldName = fieldName;	// Name of field where levelCode is stored (String)
    this.parent = parent;	// SelectSet object that manages this object

    this.next    = null;	// Select object (to the right)
    this.prev    = null;	// Select object (to the left)
    this.levelCode = null;	// Surrently select value (levelCode string?)
    this.menus   = Array();	// Array of Menu objects

    // -- Methods

    this.getParent    = function() { return this.parent;              }
    this.setLevelCode   = function( levelCode ) { this.levelCode = levelCode; }
    this.getLevelCode   = function() { return this.levelCode;             }
    this.getMenuCount = function() { return this.menus.length;        }
    this.addMenu      = Select_addMenu;
    this.show         = Select_show;
    this.hide         = Select_hide;
    this.disableNext  = Select_disableNext;
    this.showFromToFields  = Select_showFromToFields;
    this.hideFromToFields  = Select_hideFromToFields;
  }

  // ----- SelectSet --------------------------------------------------------

  function SelectSet_addSelect() {
    var fieldName   = 'asp_sel_' + this.aspectId + '_' + this.selects.length;
    var frFieldName = 'asp_val_fr_' + this.aspectId + '_' + this.selects.length;
    var toFieldName = 'asp_val_to_' + this.aspectId + '_' + this.selects.length;

    var select    = new Select( fieldName, frFieldName, toFieldName, this );
    var last      = this.getLast();
    if ( last ) {
      select.prev = last;
      last.next = select;
    }
    if ( this.debugFlag ) {
      document[ this.formName ][ this.levelCodeFieldName ].style.display = '';
      document[ this.formName ][ this.valueIdFieldName   ].style.display = '';
    } else {
      document[ this.formName ][ this.levelCodeFieldName ].style.display = 'none';  
      document[ this.formName ][ this.valueIdFieldName   ].style.display = 'none';
    }
    this.selects[ this.selects.length ] = select;
    return select;
  }

  function SelectSet_addMenu( menu ) { this.menus[ menu.classId ] = menu; }

  function SelectSet_get( level ) { 
    var length = this.selects.length;
    for ( var i=length; i<=level; i++ ) {
      this.addSelect();
    }
    return this.selects[ level ];
  }

  function SelectSet_getFirst() { 
    return this.selects.length ? this.selects[ 0 ] : null; 
  }

  function SelectSet_getLast() { 
    return this.selects.length ? this.selects[ this.selects.length-1 ] : null; 
  }

  function SelectSet_init() {
    var select = this.getFirst();
    var count  = select.getMenuCount();

    if ( count != 1 ) {
      alert( "SelectSet_init: ERROR: invalid menu count: " + select.fieldName + ": " + count );
      return;
    }
    var menu = select.menus[ 0 ].init(); 	// Display '-Select-' in first select.
    select.disableNext();			// Disable the rest of the selects

    // -- Setup menus based on the initial code value

    code = document[ this.formName ][ this.codeFieldName ].value; // Get initial value

    // AHAHAH alert( "SelectSet_init: aspectId: " + this.aspectId + ": code: '" + code + "'" );

    if ( code != '' ) {
      var levelCode = this.levelCodesByCodes[ code ];
      var valueId   = this.valueIdsByCodes[   code ];
      var menu      = this.menusByCodes[      code ];
      if ( menu == null ) { // If invalid code then clear fields
        //alert( "SelectSet_init: WARNING: unable to find menu for code: '" + code + "'" );
        document[ this.formName ][ this.levelCodeFieldName ].value = '';
        document[ this.formName ][ this.valueIdFieldName   ].value = '';
        document[ this.formName ][ this.codeFieldName      ].value = '';
        select.hideFromToFields();
        return;
      } else { // code valid, we have a levelCode and value
        document[ this.formName ][ this.levelCodeFieldName ].value = levelCode;
        document[ this.formName ][ this.valueIdFieldName   ].value = valueId;
        menu.setSelected( levelCode );
      }
    } else { // code is empty, clear levelCode and value fields
      document[ this.formName ][ this.levelCodeFieldName ].value = '';
      document[ this.formName ][ this.valueIdFieldName   ].value = '';
      select.hideFromToFields();
    }
  }

// --- Debug functions

function dumpVal( tag, a ) { // AHAHAH
  s = tag + ":\n";
  for( k in a ) {
    s += "k=" + k + ", v=" + a[k] + "\n"
  }
  alert( "dumpVal: " + s );
}

function showVal( tag, a, key ) { // AHAHAH
  alert( "showVal: " + tag + ": key=" + key + ", val=" + a[key] );
}

// -------------------

  function SelectSet_update( element ) { // Called on 'onchange' in SelectSet
    var valueString = element.value;
    var classId   = "";
    var levelCode = "";
    var valueId   = "";

    var x = valueString.split( "@" );
    if ( x.length == 2 ) {
      classId = x[0];
      levelCode = x[1];
    }

    var menu = this.menus[ classId ];
    if ( ! menu ) {
      alert( "ERROR: SelectSet_update: no menu for class id: " + classId );
      return;
    }

    var select = menu.getSelect();
    select.setLevelCode( levelCode );

    if ( levelCode == -1 ) {   			// 'Select' selected
      if ( select.prev ) {
        classId   = menu.getParentClassId(); 	// Select left-hand menu
        levelCode = select.prev.levelCode;
      } else {
        classId   = null; 			// Nothing selected
        levelCode = null;
        valueId   = null;
      }
      // -- Hide from/to fields
     
      document[ this.formName ][ select.frFieldName ].style.display= 'none';
      document.getElementById( select.toFieldName ).style.display= 'none';
    }

    var item = menu.getItem( levelCode );

    if ( item && item.subMenu ) {
      item.subMenu.init();
      var next = item.subMenu.getSelect();
      if ( item.subMenu.length == 1 ) { // If sub-menu has only one item
        classId = item.subMenu.classId; // ..then pre-select it
        levelCode = item.subMenu.items[0].levelCode;
        var subItem    = item.subMenu.getFirstItem();
        var subSubMenu = subItem.getSubMenu();
        if ( subSubMenu ) { // If sub-menu has sub-menu
          subSubMenu.init();
        } else {
          next.disableNext();
        }
      }
    } else {
      select.disableNext();
    }

    code    = this.codesByLevelCodes[ levelCode ] ? this.codesByLevelCodes[ levelCode ] : "";
    valueId = this.valueIdsByCodes[   code      ] ? this.valueIdsByCodes[    code      ] : "";
    document[ this.formName ][ this.levelCodeFieldName ].value = levelCode;
    document[ this.formName ][ this.valueIdFieldName   ].value = valueId;
    document[ this.formName ][ this.codeFieldName      ].value = code;

    select.showFromToFields( item );
  }

  function SelectSet_addCode( menu, valueId, levelCode, code ) {
    if ( this.codesByLevelCodes[ code ] != null ) {
      alert( "SelectSet_addCode: ERROR: duplicate levelCode: '" + code + "', value: '" + levelCode + "'" );
    }
    if ( this.codesByLevelCodes[ code ] != null ) {
      alert( "SelectSet_addCode: ERROR: duplicate code: '" + code + "', value: '" + levelCode + "'" );
    }
    this.codesByLevelCodes[ levelCode ] = code;
    this.levelCodesByCodes[  code     ]  = levelCode;
    this.valueIdsByCodes[    code     ]  = valueId;
    this.menusByCodes[       code     ] = menu;
  }

  function SelectSet_getMenuByLevelCode( levelCode ) { 
    var code = this.codesByLevelCodes[ levelCode ];
    return code ? this.menusByCodes[ code ] : null;
  }
  
  function SelectSet( formName, aspectId, unselectedText, debugFlag ) {

    // -- Atributes

    this.formName       = formName;		// Name of enclosing form (String)
    this.aspectId       = aspectId;		// aspectId (int)
    this.unselectedText = unselectedText;       // Text to display when unselected (String)
    this.debugFlag      = debugFlag;		// Set to true to display hidden lavelCode field

    this.levelCodeFieldName = 'levelCode_'  + aspectId;    // Name of hidden levelCode field
    this.valueIdFieldName   = 'aspectValueId_' + aspectId; // Name of hidden aspectValueId field
    this.codeFieldName      = 'code_' + aspectId;          // Name of code field for user entry/display

    this.selects = Array(); // 0 is left-most, n is right-most select (array of Select objects)
    this.menus   = Array(); // Index is classId (array of Menu objects)

    // Lookup arrays for (re-)setting menus based code value

    this.codesByLevelCodes = Array(); // Index is levelCode, value is code
    this.levelCodesByCodes = Array(); // Index is code, value is levelCode
    this.valueIdsByCodes   = Array(); // Index is code, value is valueId
    this.menusByCodes      = Array(); // Index is code, value is Menu object

    // -- Methods

    this.addSelect   = SelectSet_addSelect;
    this.addMenu     = SelectSet_addMenu;
    this.addCode     = SelectSet_addCode;
    this.getFormName = function() { return this.formName; };
    this.getUnselectedText = function() { return this.unselectedText; };
    this.get         = SelectSet_get;
    this.getFirst    = SelectSet_getFirst;
    this.getLast     = SelectSet_getLast;
    this.init        = SelectSet_init;
    this.update      = SelectSet_update;
    this.getMenuByLevelCode = SelectSet_getMenuByLevelCode;
  }
