// The global array of objects that have been instanciated
if (!Bs_Objects) {var Bs_Objects = [];};

/**
* A "better" multiple select field.
* website: http://www.blueshoes.org/en/javascript/flipflop/
* 
* 
* <pre>
* PHP developers note:
*   your field name needs to end with the [] brackets. read:
*   http://www.php.net/manual/en/faq.html.php#faq.html.select-multiple
*   "How do I get all the results from a select multiple HTML tag?"
* </pre>
* 
* 
* <b>Includes (+Dependences):</b>
* <code>
*   <script type="text/javascript" src="/_bsJavascript/core/lang/Bs_Misc.lib.js"></script>
*   <script type="text/javascript" src="/_bsJavascript/core/form/Bs_FormFieldSelect.class.js"></script>
*   <script type="text/javascript" src="/_bsJavascript/components/flipflop/Bs_FlipFlop.class.js"></script>
*   <script type="text/javascript" src="/_bsJavascript/components/toolbar/Bs_Button.class.js"></script>
* </code>
* 
* @author     andrej arn <andrej-at-blueshoes-dot-org>
* @package    javascript_components
* @subpackage flipflop
* @copyright  blueshoes.org
*/
function Bs_FlipFlop() {
	
  /**
  * tells if the field is disabled or not.
  * @access private
  * @var    bool _disabled (default is false)
  * @see    setDisabled(), getDisabled()
  * @since  bs4.5
  */
  this._disabled = false;
  
	/**
	* reference to the 'select' button.
	* @access public
	* @var    object (instance of Bs_Button)
	*/
	this.buttonSelect;
	
	/**
	* reference to the 'select-all' button.
	* @access public
	* @var    object (instance of Bs_Button)
	*/
	this.buttonSelectAll;
	
	/**
	* reference to the 'de-select' button.
	* @access public
	* @var    object (instance of Bs_Button)
	*/
	this.buttonDeselect;
	
	/**
	* reference to the 'de-select-all' button.
	* @access public
	* @var    object (instance of Bs_Button)
	*/
	this.buttonDeselectAll;
	
	/**
	* the css class name to use for the 'available' select field.
	* @access public
	* @var    string fieldAvailableCssClass
	*/
	this.fieldAvailableCssClass;
	
	/**
	* the css class name to use for the 'selected' select field.
	* @access public
	* @var    string fieldSelectedCssClass
	*/
	this.fieldSelectedCssClass;
	
	/**
	* the caption above the 'available' field.
	* @access public
	* @var    string textAvailable
	*/
	this.textAvailable = 'Available:';
	
	/**
	* css class name for the this.textAvailable string.
	* @access public
	* @var    string textAvailableCssClass
	* @see    var textAvailable
	*/
	this.textAvailableCssClass;
	
	/**
	* the caption above the 'selected' field.
	* @access public
	* @var    string textSelected
	*/
	this.textSelected = 'Selected:';
	
	/**
	* css class name for the this.textSelected string.
	* @access public
	* @var    string textAvailableCssClass
	* @see    var textSelected
	*/
	this.textSelectedCssClass;
	
	/**
	* if your select field is too small to show the full captions of the options (width), 
	* you can enable this feature. it will show the full caption of the last  
	* selected option (of both fields, 'available' and 'selected') below the fields.
	* have a look at the examples.
	* 
	* @access public
	* @see    var captionLineClass
	*/
	this.showCaptionLine = false;
	
	/**
	* the css class for the this.showCaptionLine feature.
	* @access public
	* @see    var showCaptionLine
	*/
	this.captionLineClass;
	
	/**
	* if the elements should be moved on a double click. 
	* moving means from available to selected and vice versa.
	* 
	* @access public
	* @var    bool moveOnDblClick
	* @see    var moveOnClick
	*/
	this.moveOnDblClick = true;
	
	
	/**
	* if the elements should be moved on a single click. 
	* moving means from available to selected and vice versa.
	* 
	* @access public
	* @var    bool moveOnClick
	* @see    var moveOnDblClick
	*/
	this.moveOnClick    = false;
	
	/**
	* you can limit the number of selectable options, for example 3.
	* then no more than 3 options can be in the selected list at a time.
	* @access public
	* @var    int maxSelectedNumber
	* @see    var this.maxSelectedWarning, this.maxSelectionReached();
	*/
	this.maxSelectedNumber;
	
	/**
	* the error message for this.maxSelectedNumber.
	* the string '__maxSelectedNumber__' is a placeholder for the number specified 
	* in this.maxSelectedNumber.
	* @access public
	* @var    string maxSelectedWarning
	* @see    var this.maxSelectedNumber, this.maxSelectionReached();
	*/
	this.maxSelectedWarning = 'No more than __maxSelectedNumber__ options can be selected.';
	
	/**
	* how to render the fields. default is left-to-right.
	* if true then top-to-bottom will be done.
	* @access private
	* @var    bool _renderTopToBottom
	* @see    this.setRenderOrientation()
	*/
	this._renderTopToBottom = false;
	
  /**
  * name of the hidden form field.
	* 
	* the method convertField() uses the field name of the field you convert. 
	* don't set it here.
	* 
	* if it does not end with the [] brackets already, they will be added.
	* multiple fields in php need that.
	* 
	* @access public
  * @var    string hiddenFieldName
  */
  this.hiddenFieldName;
	
	/**
	* reference to the "available" field element.
	* @access private
	* @var    element _fldAvailable
	*/
	this._fldAvailable;
	
	/**
	* reference to the "selected" field element.
	* @access private
	* @var    element _fldSelected
	*/
	this._fldSelected;
	
	
  /**
  * array holding all the information about attached events. 
	* 
  * the structure can be like these:
  * 1) attach a function directly
  *    syntax:  _attachedEvents['eventName'] = yourFunctionName;
  * 2) attach some javascript code
  *    syntax:  _attachedEvents['eventName'] = "yourCode();";
  *    example: _attachedEvents['eventName'] = "alert('hi'); callSomething('foo');";
  *    just keep in mind that you cannot use vars in that code, because when it 
  *    gets executed that will be another scope (unless the vars are global...)
  * 3) attach multiple things for the same event
  *    syntax:  _attachedEvents['eventName']    = new Array;
  *             _attachedEvents['eventName'][0] = yourFunctionName;
  *             _attachedEvents['eventName'][1] = "yourCode();";
  * 
  * @access private
  * @var    array _attachedEvents (hash, see above)
  * @see    this.attachEvent();
  */
  this._attachedEvents;	
	
	/**
	* temporary helper var for _onClickAvailableOption() and _onClickSelectedOption(). 
	* @access private
	* @var    element _lastOnClickOptionElm
	* @see    this._onClickAvailableOption(), this._onClickSelectedOption()
	*/
	this._lastOnClickOptionElm;
	
	
	/**
	* constructor.
	*/
	this._constructor = function() {
  	// Put this instance into the global object instance list
    this._id = Bs_Objects.length;
    Bs_Objects[this._id] = this; 
    this._objectId = "Bs_FlipFlop_"+this._id;

		this.buttonSelect = new Bs_Button();
		this.buttonSelect.objectName      = this._objectId + '_btnSel';
	  this.buttonSelect.imgName         = 'bs_forward.gif';
	  this.buttonSelect.title           = 'Select';
		this.buttonSelect.cssClassDefault = 'bsBtnMouseOver';
		this.buttonSelect.attachEvent('Bs_Objects['+this._id+'].selectSelected();');
		
		this.buttonSelectAll = new Bs_Button();
		this.buttonSelectAll.objectName      = this._objectId + '_btnSelAll';
	  this.buttonSelectAll.imgName         = 'bs_forforward.gif';
	  this.buttonSelectAll.title           = 'Select All';
		this.buttonSelectAll.cssClassDefault = 'bsBtnMouseOver';
		this.buttonSelectAll.attachEvent('Bs_Objects['+this._id+'].selectAll();');
		
		this.buttonDeselect = new Bs_Button();
		this.buttonDeselect.objectName      = this._objectId + '_btnDe';
	  this.buttonDeselect.imgName         = 'bs_back.gif';
	  this.buttonDeselect.title           = 'Deselect';
		this.buttonDeselect.cssClassDefault = 'bsBtnMouseOver';
		this.buttonDeselect.attachEvent('Bs_Objects['+this._id+'].deselectSelected();');
		
		this.buttonDeselectAll = new Bs_Button();
		this.buttonDeselectAll.objectName      = this._objectId + '_btnDeAll';
	  this.buttonDeselectAll.imgName         = 'bs_baback.gif';
	  this.buttonDeselectAll.title           = 'Deselect All';
		this.buttonDeselectAll.cssClassDefault = 'bsBtnMouseOver';
		this.buttonDeselectAll.attachEvent('Bs_Objects['+this._id+'].deselectAll();');
	}
	
	
	/**
	* replaces the existing select field specified with the flipflop fields.
	* @access public
	* @param  string elementId
	* @return void
	*/
	this.convertField = function(elementId) {
		var origFld = document.getElementById(elementId);
		if (origFld.disabled) this.setDisabled(true);

		this.hiddenFieldName = origFld.name;
		if (this.hiddenFieldName.substr(this.hiddenFieldName.length -2) != '[]') this.hiddenFieldName += '[]';
		
		var s = new Bs_FormFieldSelect();
		s.init(origFld);
		
		var fldOne = '<select name="' + elementId + '_fldAvailable" id="' + elementId + '_fldAvailable" size="' + origFld.size + '" multiple';
		fldOne += ' onclick="Bs_Objects['+this._id+'].onClickAvailable(this);"';
		fldOne += ' ondblclick="Bs_Objects['+this._id+'].onDblClickAvailable();"';
		if (typeof(this.fieldAvailableCssClass) != 'undefined') {
			fldOne += ' class="' + this.fieldAvailableCssClass + '"';
		}
    if (this._disabled) fldOne += ' disabled';
		fldOne += '>';
		
		var fldTwo = '<select name="' + elementId + '_fldSelected" id="' + elementId + '_fldSelected" size="' + origFld.size + '" multiple';
		fldTwo += ' onclick="Bs_Objects['+this._id+'].onClickSelected(this);"';
		fldTwo += ' ondblclick="Bs_Objects['+this._id+'].onDblClickSelected();"';
		if (typeof(this.fieldSelectedCssClass) != 'undefined') {
			fldTwo += ' class="' + this.fieldSelectedCssClass + '"';
		}
    if (this._disabled) fldTwo += ' disabled';
		fldTwo += '>';
		
		var defaultsArr = new Array;
    for (var i=0; i<origFld.options.length; i++) {
			var key = origFld.getValueOrText(i);
			if (origFld.options[i].selected) {
				defaultsArr[defaultsArr.length] = key;
				fldTwo += '<option value="' + key + '">' + origFld.options[i].text + '</option>'; //  ondragstart="alert(\'dragstart\');"
			} else {
				fldOne += '<option value="' + key + '">' + origFld.options[i].text + '</option>';
			}
    }
		
		fldOne += '</select>';
		fldTwo += '</select>';
		
		var out = '';
		out += '<table border="0" cellspacing="0" cellpadding="0"><tr><td valign="top"';
		if (typeof(this.textAvailableCssClass) != 'undefined') out += ' class="' + this.textAvailableCssClass + '"';
		out += '>';
		out += this.textAvailable;
		out += '</td><td>&nbsp;</td>';
		
		var textSelectedWithTd = '';
		textSelectedWithTd += '<td valign="top"';
		if (typeof(this.textSelectedCssClass) != 'undefined') textSelectedWithTd += ' class="' + this.textSelectedCssClass + '"';
		textSelectedWithTd += '>';
		textSelectedWithTd += this.textSelected;
		textSelectedWithTd += '</td>';
		
		if (!this._renderTopToBottom) {
			out += textSelectedWithTd;
		}
		out += '</tr><tr><td valign="top">';
		out += fldOne;
		out += '</td>';
		
		if (this._renderTopToBottom) out += '</tr><tr>';
		
		var buttonDivStyle = (this._renderTopToBottom) ? ' style="display:inline;"' : '';
		out += '<td align="center" valign="middle">';
		out += '<div id="' + elementId + '_btnSel"'    + buttonDivStyle + '></div>';
		out += '<div id="' + elementId + '_btnSelAll"' + buttonDivStyle + '></div>';
		out += '<div id="' + elementId + '_btnDe"'     + buttonDivStyle + '></div>';
		out += '<div id="' + elementId + '_btnDeAll"'  + buttonDivStyle + '></div>';
		out += '</td>';
		
		if (this._renderTopToBottom) out += '</tr><tr>';
		
		if (this._renderTopToBottom) {
			out += textSelectedWithTd;
			out += '</tr><tr>';
		}
		
		out += '<td valign="top">';
		out += fldTwo;
		out += '</td></tr>';
		if (this.showCaptionLine) {
			out += '<tr><td colspan="3"';
			if (typeof(this.captionLineClass) != 'undefined') {
				out += 'class="' + this.captionLineClass + '"';
			}
			out += ' id="' + this._objectId + '_captionLine"';
			out += '>';
			out += '</td></tr>';
		}
		out += '</table>';
		
		out += '<span id="' + this._objectId + '_hiddenFldSpan">';
		out += this._renderHiddenField(defaultsArr);
		out += '<span>';
		
		origFld.outerHTML = out;
		this._fldAvailable = document.getElementById(elementId + '_fldAvailable');
		this._fldSelected  = document.getElementById(elementId + '_fldSelected');
		s.init(this._fldAvailable);
		s.init(this._fldSelected);
		
		if (!this.moveOnClick) {
		  this.buttonSelect.drawInto(elementId + '_btnSel');
			eval(this._objectId + '_btnSel' + ' = Bs_Objects['+this._id+'].buttonSelect;');
		  this.buttonDeselect.drawInto(elementId + '_btnDe');
			eval(this._objectId + '_btnDe' + ' = Bs_Objects['+this._id+'].buttonDeselect;');
		}
		if (typeof(this.maxSelectedNumber) == 'undefined') {
      try {
  		  this.buttonSelectAll.drawInto(elementId + '_btnSelAll');
  			eval(this._objectId + '_btnSelAll' + ' = Bs_Objects['+this._id+'].buttonSelectAll;');
  	  	this.buttonDeselectAll.drawInto(elementId + '_btnDeAll');
  			eval(this._objectId + '_btnDeAll' + ' = Bs_Objects['+this._id+'].buttonDeselectAll;');
      } catch (e) {
        //never mind, buttons may be unset, so they are not used.
      }
		}
	}
	
	
	/**
	* returns an array with the keys of the selected elements as vector.
	* @access public
	* @return array (vector)
	*/
	this.getValue = function() {
		return this._fldSelected.getAllKeys();
	}
	
	
	/**
	* 
	* @access public
	* @param  array options (hash)
	* @return ?
	* @since  bs4.5
	*/
	this.setOptions = function(options) {
		var selected = this._fldSelected.getAllKeys();
		this._fldAvailable.prune();
		this._fldSelected.prune();
		for (var i=0; i<selected.length; i++) {
			if (typeof(options[selected[i]]) != 'undefined') {
				var newOpt = new Option(options[selected[i]], selected[i], false, false);
				this._fldSelected.options[this._fldSelected.length] = newOpt;
				delete options[selected[i]];
			}
			/*
			var pos = options.indexOf(selected[i]);
			alert(selected[i]);
			if (pos >= 0) {
        //var newOpt = new Option(options.text, .value, false, false);
				//this._fldSelected.options[this._fldSelected.length] = newOpt;
			}
			*/
		}
		
		for (var key in options) {
			var newOpt = new Option(options[key], key, false, false);
			this._fldAvailable.options[this._fldAvailable.length] = newOpt;
		}
	}
	
	/**
	* moves the selected items of the 'available' field to the 'selected' field.
	* @access public
	* @return void
	*/
	this.selectSelected = function() {
		this._fldAvailable.moveSelectedTo(this._fldSelected);
		this._updateHiddenField();
	}
	
	/**
	* moves all items of the 'available' field to the 'selected' field.
	* @access public
	* @return void
	*/
	this.selectAll = function() {
		this._fldAvailable.moveAllTo(this._fldSelected);
		this._updateHiddenField();
	}
	
	/**
	* moves the selected items of the 'selected' field to the 'available' field.
	* @access public
	* @return void
	*/
	this.deselectSelected = function() {
		this._fldSelected.moveSelectedTo(this._fldAvailable);
		this._updateHiddenField();
	}
	
	/**
	* moves all items of the 'selected' field to the 'available' field.
	* @access public
	* @return void
	*/
	this.deselectAll = function() {
		this._fldSelected.moveAllTo(this._fldAvailable);
		this._updateHiddenField();
	}
	
	/**
	* moves the option specified from the 'available' field to the 'selected' field.
	* @access public
	* @param  string optionValue (the .value attribute of the option you want to move)
	* @return bool
	*/
	this.moveAvailableOptionToSelected = function(optionValue) {
		if (this.maxSelectionReached()) {
			this.alertMaxSelectionWarning();
		} else {
			var status = this._fldAvailable.moveTo(this._fldSelected, optionValue);
			this._updateHiddenField();
			return status;
		}
	}
	
	/**
	* moves the option specified from the 'selected' field to the 'available' field.
	* @access public
	* @param  string optionValue (the .value attribute of the option you want to move)
	* @return bool
	*/
	this.moveSelectedOptionToAvailable = function(optionValue) {
		var status = this._fldSelected.moveTo(this._fldAvailable, optionValue);
		this._updateHiddenField();
		return status;
	}
	
	/**
	* Adds elements to the flipflop fields (at runtime).
	* @access public
	* @param  array dataHash
	* @param  bool selected (default is false which adds to the 'available' field.)
	* @param  bool posStart (default is false which adds at bottom)
	* @return void
	*/
	this.addElementsByHash = function(dataHash, selected, posStart) {
		if (selected) {
			this._fldSelected.addElementsByHash(dataHash, posStart);
		} else {
			this._fldAvailable.addElementsByHash(dataHash, posStart);
		}
	}
	
	/**
	* fires on a single click on the 'available' select field.
	* @access public (you don't need that)
	* @return void
	*/
	this.onClickAvailable = function() {
    if (this.disabled) return;
		this._onClickAvailableOption();
	}
	
	/**
	* fires on a single click on the 'selected' select field.
	* @access public (you don't need that)
	* @return void
	*/
	this.onClickSelected = function() {
    if (this.disabled) return;
		this._onClickSelectedOption();
	}
	
	/**
	* fires on a double click on the 'available' select field.
	* @access public (you don't need that)
	* @return void
	*/
	this.onDblClickAvailable = function() {
    if (this.disabled) return;
		if (this.moveOnDblClick) this.moveAvailableOptionToSelected(event.srcElement.value);	
	}
	
	/**
	* fires on a double click on the 'selected' select field.
	* @access public (you don't need that)
	* @return void
	*/
	this.onDblClickSelected = function() {
    if (this.disabled) return;
		if (this.moveOnDblClick) this.moveSelectedOptionToAvailable(event.srcElement.value);	
	}
	
	/**
	* fires on a single click on an option in the 'available' select field.
	* @access  private
	* @return  void
	*/
	this._onClickAvailableOption = function(b) {
		//problem here: the click happened, the method fired, but selectedIndex is not updated yet. 
		//timing problem. if we wait a bit (0.1 secs or so) it's ready. thus we use the setTimeout to try again.
		if (b == true) {
			try {
				var srcElm = (!bs_isNull(event) && !bs_isNull(event.srcElement)) ? event.srcElement : this._lastOnClickOptionElm;
				var opt    = srcElm.options[srcElm.selectedIndex];
				if (this.showCaptionLine) {
					document.getElementById(this._objectId + '_captionLine').innerHTML = opt.text; //opt.value
				}
			} catch (e) {
				//does not work in moz.
			}
			
			if (this.moveOnClick) {
				this.moveAvailableOptionToSelected(srcElm.value);	
			}
		} else {
			this._lastOnClickOptionElm = event.srcElement;
			setTimeout('Bs_Objects['+this._id+']._onClickAvailableOption(true)', 50);
			return;
		}
	}
	
	/**
	* fires on a single click on an option in the 'selected' select field.
	* @access  private
	* @return  void
	*/
	this._onClickSelectedOption = function(b) {
		//problem here: the click happened, the method fired, but selectedIndex is not updated yet. 
		//timing problem. if we wait a bit (0.1 secs or so) it's ready. thus we use the setTimetout to try again.
		if (b == true) {
			try {
				var srcElm = (!bs_isNull(event) && !bs_isNull(event.srcElement)) ? event.srcElement : this._lastOnClickOptionElm;
				var opt    = srcElm.options[srcElm.selectedIndex];
				if (this.showCaptionLine) {
					document.getElementById(this._objectId + '_captionLine').innerHTML = opt.text; //opt.value
				}
			} catch (e) {
				//does not work in moz.
			}
			
			if (this.moveOnClick) {
				this.moveSelectedOptionToAvailable(srcElm.value);	
			}
		} else {
			this._lastOnClickOptionElm = event.srcElement;
			setTimeout('Bs_Objects['+this._id+']._onClickSelectedOption(true)', 50);
			return;
		}
	}
	
	
	/**
	* how to render the fields. default is left-to-right.
	* if true then top-to-bottom will be done.
	* 
	* @access public
	* @var    bool upDown
	* @return void
	* @see    var this._renderTopToBottom
	*/
	this.setRenderOrientation = function(upDown) {
		this._renderTopToBottom = upDown;
	  this.buttonSelect.imgName         = (this._renderTopToBottom) ? 'bs_down.gif'   : 'bs_forward.gif';
	  this.buttonSelectAll.imgName      = (this._renderTopToBottom) ? 'bs_dodown.gif' : 'bs_foforward.gif';
	  this.buttonDeselect.imgName       = (this._renderTopToBottom) ? 'bs_up.gif'     : 'bs_back.gif';
	  this.buttonDeselectAll.imgName    = (this._renderTopToBottom) ? 'bs_upup.gif'   : 'bs_baback.gif';
	}
	
	
	/**
	* tells if the this.maxSelectedNumber is reached already.
	* @access public
	* @return bool
	* @see    vars this.maxSelectedNumber, this.maxSelectedWarning
	*/
	this.maxSelectionReached = function() {
		return (this.howManySelected() >= this.maxSelectedNumber);
	}
	
	/**
	* alerts the "max selection reached" warning. no more options can be selected, unless 
	* others are dropped.
	* @access public (if you like)
	* @return void
	*/
	this.alertMaxSelectionWarning = function() {
		alert(this.maxSelectedWarning.replace('__maxSelectedNumber__', this.maxSelectedNumber));
	}
	
	/*
	this.checkMaxSelectionAfter = function() {
	}*/
	
	/**
	* tells how many options are currently selected.
	* @access public
	* @return int (0-n)
	*/
	this.howManySelected = function() {
		return this._fldSelected.options.length;
	}
	
  
  /**
  * disables/enables the field.
  * @access public
  * @param  bool disabled
  * @see    getDisabled(), var _disabled
  * @since  bs4.5
  */
  this.setDisabled = function(disabled) {
    this._disabled = disabled;
    if (this._disabled) {
      var btnValue = 0;
    } else {
      var btnValue = 1;
    }
    //disable the buttons:
    if (!bs_isNull(this.buttonSelect))      this.buttonSelect.setStatus(btnValue);
    if (!bs_isNull(this.buttonSelectAll))   this.buttonSelectAll.setStatus(btnValue);
    if (!bs_isNull(this.buttonDeselect))    this.buttonDeselect.setStatus(btnValue);
    if (!bs_isNull(this.buttonDeselectAll)) this.buttonDeselectAll.setStatus(btnValue);
    //disable the select fields:
    if (!bs_isNull(this._fldAvailable)) this._fldAvailable.disabled = this._disabled;
    if (!bs_isNull(this._fldSelected))  this._fldSelected.disabled  = this._disabled;
  }
  
  /**
  * disables/enables the field.
  * @access public
  * @return bool
  * @see    setDisabled(), var _disabled
  * @since  bs4.5
  */
  this.getDisabled = function() {
    return this._disabled;
  }
  
	
	/**
	* @access private
	* @param  array defaultsArr
	* @return string
	*/
	this._renderHiddenField = function(defaultsArr) {
		var ret = new Array;
		for (var i=0; i<defaultsArr.length; i++) {
			ret[ret.length] = '<input type="hidden" name="' + this.hiddenFieldName + '" value="' + defaultsArr[i] + '">';
		}
		return ret.join('');
	}
	
	/**
	* updates the value of the hidden field.
	* @access private
	* @return void
	*/
	this._updateHiddenField = function() {
		var defaultsArr = this._fldSelected.getAllKeys();
		var str         = this._renderHiddenField(defaultsArr);
		document.getElementById(this._objectId + '_hiddenFldSpan').innerHTML = str;
	}
	
	
  /**
  * attaches an event.
	* 
	* the following triggers can be used:
	*   'onBeforeChange'
	*   'onAfterChange'
	* 
	* the onXXXChange events fire when the wheel is used, a button (up/down) 
	* is clicked, cursor up or down is pushed. but they do NOT fire when you 
	* use the api methods setValue(), increase() and decrease(). because 
	* then you already know what's happening.
	* 
	* 
	* the events will be executed in the order they were registered.
	* 
	* if an onBeforeXXX event you've attached returns bool FALSE, it 
	* will stop executing any other attached events in that queue, 
	* and it will quit. example: if you attach an onBeforeChange 
	* event, and your code returns FALSE, the change won't be done 
	* at all.
	* 
	* examples:
	*   myObj.attachEvent('onBeforeChange', myFunction);
	*   then your function myFunction() receives one param, it is 
	*   a reference to this object (myObj).
	*   
	*   myObj.attachEvent('onBeforeChange', "if (true) return false;");
	*   this is an example with code attached that will be evaluated.
	* 
  * @access public
  * @param  string trigger
  * @param  mixed  yourEvent (string (of code) or function)
  * @return void
  * @see    var this._attachedEvents
  */
  this.attachEvent = function(trigger, yourEvent) {
    if (typeof(this._attachedEvents) == 'undefined') {
      this._attachedEvents = new Array();
    }
    
    if (typeof(this._attachedEvents[trigger]) == 'undefined') {
      this._attachedEvents[trigger] = new Array(yourEvent);
    } else {
      this._attachedEvents[trigger][this._attachedEvents[trigger].length] = yourEvent;
    }
  }
	
  /**
  * tells if any event is attached for the trigger specified. 
  * @access public
  * @param  string trigger
  * @return bool
  */
  this.hasEventAttached = function(trigger) {
    return (this._attachedEvents && this._attachedEvents[trigger]);
  }
  
  /**
  * fires the events for the trigger specified.
  * @access public (used internally but feel free to trigger events yourself...)
  * @param  string trigger
  * @return void
  */
  this.fireEvent = function(trigger) {
    if (this._attachedEvents && this._attachedEvents[trigger]) {
      var e = this._attachedEvents[trigger];
      if ((typeof(e) == 'string') || (typeof(e) == 'function')) {
        e = new Array(e);
      }
      for (var i=0; i<e.length; i++) {
        if (typeof(e[i]) == 'function') {
          var status = e[i](this);
        } else if (typeof(e[i]) == 'string') {
          var status = eval(e[i]);
        } //else murphy
				if (status == false) return false;
      }
    }
		return true;
  }	
	
	
	this._constructor(); //call the constructor. needs to be at the end.
	
}

