Source: inputElement.js

/**
 * @fileOverview This file defines the InputElement class.
 * Copyright 2008-2015 by Kenneth F. Greenberg; all rights reserved
 * This work is licensed under the Creative Commons Attribution-Share Alike License. To view a copy of this license, 
 * visit http://creativecommons.org/licenses/by-sa/3.0/; 
 * or, (b) send a letter to Creative Commons, 171 2nd Street, Suite 300, San Francisco, California, 94105, USA.
 * @author Kenneth F. Greenberg
 */
/*
	Input elements start with a primitive base type and add to it to produce specific types.
	The generic input element has a single property, the input node. This represents the DOM element
	which is placed somewhere in the tree. It has the method appendTo() and (since it is
	always an input type), methods for attaching event listeners to it. There is a generic function
	for adding event listeners in a browser-independent way that is part of this module.
	
	There is also a derived type for creating an input element with a label. This adds a label
	property and methods setLabel and setTextAlign.
	
	Some primitive types like hidden and button do not allow the use of labels, and derive
	directly from the base class. Others, like textbox, derive from the label class. Of course, 
	just because a label is possible does not mean it must be used.
	
	There are also classes defined here that are not strictly speaking inputs (like DDL, which
	is a select). They are so similar that we include them in a consistent way, but they 
	cannot be derived from the base class.

*/
/*global document,Option */
// This function adds a listener to an element in a browser-agnostic way
// elem is an element; ev is a string like click or change; fn is the function to call
function addInputElementListener(elem,ev,fn) {
	if (elem.addEventListener ) {
	  elem.addEventListener(ev, fn, false); 
	} else if (elem.attachEvent) {
		  elem.attachEvent('on'+ev, fn);
	}
}
/**
 * @class The generic input element, from which others are derived
 */
function InputElement(tag) {
	this.input = document.createElement('input');
	this.input.setAttribute('id',tag);
	this.input.setAttribute('name',tag);
}
InputElement.prototype.input = null;
/**
 * Appends the derived object to a DOM node
 * @param {Object} parent The DOM element to which this is added
 */
InputElement.prototype.appendTo = function(parent) { parent.appendChild(this.input); };
/**
 * Attaches an onclick handler to the object
 * @param {function} listener The event listener to add
 */
InputElement.prototype.onClick = function(listener) { addInputElementListener(this.input,'click',listener); };
/**
 * Attaches an onchange handler to the object
 * @param {function} listener The event listener to add
 */
InputElement.prototype.onChange = function(listener) { addInputElementListener(this.input,'change',listener); };
/**
 * Disables the object
 */
InputElement.prototype.disable = function() { this.input.disabled = true; };
/**
 * Enables the object
 */
InputElement.prototype.enable = function() { this.input.disabled = false; };
/*
 * Getter for associated GUI element
 */
InputElement.prototype.getGUI= function() { return this.input.getAttribute("type"); };
/**
 * @class A hidden input object. Used to pass non-displayed info to server.
 * @extends InputElement
 * @param {String} tag The name of the element
 * @param {mixed} val The value of the element
 * @example customerID = new InputElementHidden('custID',customerID);
 */
function InputElementHidden(tag,val) {
	this.inheritFrom = InputElement;
	this.inheritFrom(tag);
	this.input.setAttribute('type','hidden');
	this.input.setAttribute('value',val);
}
InputElementHidden.prototype = new InputElement();	// inherit from base type
/**
 * Retrieves the value previously attached to the object.
 * @returns {mixed} The attached value
 */
InputElementHidden.prototype.getValue = function() { return this.input.getAttribute("value"); };
/**
 * Changes the value of the object, originally set in the constructor.
 * @param {String} valuestring The value to be assigned to the object
 */
InputElementHidden.prototype.setValue = function(valuestring) { this.input.setAttribute("value",valuestring); };
/**
 * @class A button object, with an optional event listener
 * @extends InputElement
 * @param {string} tag The name of the element
 * @param {string} label The text displayed in the button
 * @example var saveButton = new InputElementButton('btnSave','Save');
 */
function InputElementButton(tag,label) {
	this.inheritFrom = InputElement;
	this.inheritFrom(tag);
	this.input.setAttribute('type','button');
	if (label) {
		this.input.setAttribute('value',label);
	} else {
		this.input.setAttribute('value',tag);
	}
}
InputElementButton.prototype = new InputElement();
InputElementButton.prototype.value = null;
/**
 * Retrieves a value previously attached to an InputElementButton object.
 * @returns {mixed} The attached value
 */
InputElementButton.prototype.getValue = function() { return this.value; };
/**
 * Attaches a value to an InputElementButton object
 * @param {mixed} v The value to attach
 */
InputElementButton.prototype.setValue = function(v) { this.value = v; };
/**
 * Changes the label of an InputElementButton object
 * @param {string} newText The new text to display in the button
 * @example if (helpIsNowDisplayed) { btnShowHelp.setLabel('Hide'); }
 */
InputElementButton.prototype.setLabel = function(newText) {	this.input.setAttribute('value',newText); };
/**
 * Changes the style of the button
 * @param {string} ss The new style to apply to the button
 */
InputElementButton.prototype.setStyle = function(ss) { this.input.setAttribute("style",ss); };
/**
 * @class A label object, since some objects have a label via composition
 */
function ElementLabel() {}	// empty constructor
ElementLabel.prototype = new ElementLabel();
ElementLabel.prototype.label = null;
ElementLabel.prototype.align = 'l';
/**
 * Sets the text for the associated label. Creates a DOM label element if not existing.
 * @param {string} text The label for the including object.
 */
ElementLabel.prototype.setLabel = function(text) {
	if (!this.label) {
		this.label = document.createElement('label');
	}
	this.label.innerHTML = text;
};
/**
 * Retrieves the text for the associated label.
 * @returns {string} The label for the including object.
 */
ElementLabel.prototype.getLabel = function() {
	if (!this.label) {
		return null;
	}
	return this.label.innerHTML;
};
/**
 * Sets the text alignment for the associated label. Default is left.
 * @param {string} text The desired alignment, either 'left' or 'right'
 */
ElementLabel.prototype.setTextAlign = function(text) {
	if ( text === 'right') {
		this.align = 'r';
	} else if ( text === 'left' ) {
		this.align = 'l';
	} 
};
/**
 * @class This is a derived type, that includes an InputElement and a label as well.
 * It IS an input element, it HAS a label.
 * @extends InputElement
 */
function InputElementWithLabel(tag) {
	this.inheritFrom = InputElement;
	this.inheritFrom(tag);
	this.labelObj = new ElementLabel();	// use composition here, since not derived
}
InputElementWithLabel.prototype = new InputElement();
/**
 * Sets the text for the associated label.
 * @param {string} text The label for the including object.
 */
InputElementWithLabel.prototype.setLabel = function(text) {
	this.labelObj.setLabel(text);
};
/**
 * Sets the text alignment for the associated label. Default is left.
 * @param {string} text The desired alignment, either 'left' or 'right'
 */
InputElementWithLabel.prototype.setTextAlign = function(text) {
	this.labelObj.setTextAlign(text);
};
/**
 * Appends the derived object and any label to a DOM node
 * @param {Object} parent The DOM element to which this is added
 */
InputElementWithLabel.prototype.appendTo = function(parent) {
	if (this.labelObj.label) {
		if (this.labelObj.align === 'l') {
			parent.appendChild(this.labelObj.label);
			parent.appendChild(this.input);
		} else if (this.labelObj.align === 'r') {
			parent.appendChild(this.input);
			parent.appendChild(this.labelObj.label);
		} else {
			parent.appendChild(this.input);
		}
	} else {
		parent.appendChild(this.input);
	}
};
/**
 * @class A textbox object, derived from labeled object
 * @extends InputElementWithLabel
 */
function InputElementTextbox(tag) {
	this.inheritFrom = InputElementWithLabel;
	this.inheritFrom(tag);
	this.input.setAttribute("type","text");
	return this;
}
InputElementTextbox.prototype = new InputElementWithLabel();	// inherit from label type
/**
 * Sets the size of the textbox.
 * @param {number} width The desired width of the textbox
 */
InputElementTextbox.prototype.setSize = function(width) { this.input.setAttribute('size',width); };
/**
 * Sets the text to be displayed when a user hovers the mouse over the textbox.
 * @param {string} text The display text
 */
InputElementTextbox.prototype.setTooltip = function(text) { this.input.setAttribute("title",text); };
/**
 * Sets the text to be displayed in the textbox.
 * @param {string} text The display text
 */
InputElementTextbox.prototype.setValue = function(valuestring) { this.input.setAttribute("value",valuestring); };
/**
 * Retrieves the text currently displayed in the textbox.
 * @returns {string} The display text
 */
InputElementTextbox.prototype.getValue = function() { return this.input.value; };
/**
 * @class An HTML5 email object, derived from the textbox object. In some browsers, treated as text
 * @extends InputElementTextbox
 */
function InputElementEmail(tag) {
	this.inheritFrom = InputElementTextbox;
	this.inheritFrom(tag);
	this.input.setAttribute("type","email");
	return this;
}
InputElementEmail.prototype = new InputElementTextbox();
/**
 * @class An HTML5 URL object, derived from the textbox object. In some browsers, treated as text
 * @extends InputElementTextbox
 */
function InputElementURL(tag) {
	this.inheritFrom = InputElementTextbox;
	this.inheritFrom(tag);
	this.input.setAttribute("type","url");
	return this;
}
InputElementURL.prototype = new InputElementTextbox();
/**
 * @class An HTML5 Search object, derived from the textbox object. In some browsers, treated as text
 * @extends InputElementTextbox
 */
function InputElementSearch(tag) {
	this.inheritFrom = InputElementTextbox;
	this.inheritFrom(tag);
	this.input.setAttribute("type","search");
	return this;
}
InputElementSearch.prototype = new InputElementTextbox();
/**
 * @class An HTML5 Date object, derived from the textbox object. In some browsers, treated as text
 * @extends InputElementTextbox
 */
function InputElementDate(tag) {
	this.inheritFrom = InputElementTextbox;
	this.inheritFrom(tag);
	this.input.setAttribute("type","date");
	return this;
}
InputElementDate.prototype = new InputElementTextbox();
/**
 Test to see if date is supported in this browser
 */
InputElementDate.prototype.isDate = function() { return this.input.type === "date"; };
/**
 * @class A checkbox object, derived from labeled object
 * @extends InputElementWithLabel
 */
function InputElementCheckbox(tag) {
	this.inheritFrom = InputElementWithLabel;
	this.inheritFrom(tag);
	this.input.setAttribute("type","checkbox");
}
InputElementCheckbox.prototype = new InputElementWithLabel();	// inherit from base type
/**
 * Sets the checkbox to checked.
 */
InputElementCheckbox.prototype.setChecked = function() { this.input.checked = true; };
/**
 * Sets the checkbox to unchecked.
 */
InputElementCheckbox.prototype.clearChecked = function() { this.input.checked = false; };
/**
 * Returns the checkbox state.
 * @returns {boolean} Whether the checkbox is checked or not
 */
InputElementCheckbox.prototype.isChecked = function() { return this.input.checked; };
/**
 * Provides a mechanism to store a value for a checkbox
 * @param {string} vstring The string value to be assigned to the checkbox
 */
InputElementCheckbox.prototype.setValue = function(vstring) { this.input.setAttribute("value",vstring); };
/**
 * Retrieves the value attribute of a checkbox
 * @returns {string} the value associated with the checkbox object
 */
InputElementCheckbox.prototype.getValue = function() { return this.input.value; };
/**
 * @class A radio object, derived from labeled object. Built as a span due to IE flakiness.
 * Normally created from the InputElementRadioGroup object.
 * @extends InputElementWithLabel
 * @param {string} tag The name and id of this object.
 */
function InputElementRadio(tag) {
	this.inheritFrom = InputElementWithLabel;
	this.inheritFrom(tag);
	// replace the input with a span element
	this.input = document.createElement('span');
	this.input.innerHTML = '<input type="radio" name="'+tag+'">';
}
InputElementRadio.prototype = new InputElementWithLabel();	// inherit from label type
/**
 * Assigns a value to a Radio object
 * @param {mixed} value The value of this object.
 */
InputElementRadio.prototype.setValue = function(value) { this.input.firstChild.value = value; };
/**
 * @class A Radio Button Group object, derived from labeled object
 * @extends InputElementWithLabel
 * @param {string} tag The name and id of this object.
 * @example filterRBG = new InputElementRadioGroup('rbgFilter');
 */
function InputElementRadioGroup(tag) {
	this.inheritFrom = InputElementWithLabel;	// label for group
	this.inheritFrom(tag);
	this.groupName = tag;
	this.buttons = [];
}
InputElementRadioGroup.prototype = new InputElementWithLabel();	// inherit from base type
/**
 * adds a new radio button to the group
 * @param {string} label The label for this radio button
 * @param {mixed} value The value to assign to this radio button
 */
InputElementRadioGroup.prototype.addButton = function(label,value) {
	var button = new InputElementRadio(this.groupName);
	button.setTextAlign('right');
	button.setLabel(label);
	if (value) {
		button.setValue(value);
	} else {
		button.setValue(label);
	}
	this.buttons.push(button);
};
/**
 * Since this does not have a corresponding input, have an explicit method
 */
InputElementRadioGroup.prototype.getGUI= function() { return "radiogroup"; };
/**
 * Selects one of the buttons under program control
 * @param {number} index The index of the radio button to select
 */
InputElementRadioGroup.prototype.selectButton = function(index) {
	if (index < this.buttons.length) {
		// input is the SPAN, firstChild is the radio button itself
		this.buttons[index].input.firstChild.checked = true;
	}
};
/**
 * Selects one of the buttons based on its label 
 * @param {number} index The label of the radio button to select
 */
InputElementRadioGroup.prototype.selectButtonByLabel = function(label) {
	var i;
	for (i=0; i<this.buttons.length; i++) {
		if (this.buttons[i].labelObj.getLabel() === label) {
			this.buttons[i].input.firstChild.checked = true;
			return;
		}
	}
};
// Retrieves the value of the currently selected radio button - DEPRECATED, use getValue()
InputElementRadioGroup.prototype.getSelected = function() {
	var i;
	
	for (i=0; i<this.buttons.length; i++) {
		if (this.buttons[i].input.firstChild.checked) {
			return this.buttons[i].input.firstChild.value;
		}
	}
	return null;
};
/**
 * Retrieves the value of the currently selected radio button
 * @returns {mixed} The value of the selected button
 */
InputElementRadioGroup.prototype.getValue = function() {
	var i;
	
	for (i=0; i<this.buttons.length; i++) {
		if (this.buttons[i].input.firstChild.checked) {
			return this.buttons[i].input.firstChild.value;
		}
	}
	return null;
};
/**
 * Retrieves the number of radio buttons in the group
 * @returns {number} The number of radio buttons in this group
 */
InputElementRadioGroup.prototype.getGroupSize = function() {
	return this.buttons.length;
};
/**
 * Retrieves the radio button specified
 * @param {number} index The InputElementRadio object to retrieve
 * @returns {object} The specified RadioButton object
 */
InputElementRadioGroup.prototype.getButton = function(index) {
	if (index < this.buttons.length) {
		return this.buttons[index];
	}
};
/**
 * Adds an onclick handler to each button in the group. This must be called AFTER all buttons 
 * have been added, since the listener is only added to known buttons.
 * @param {function} listener The function to call on button click
 */
InputElementRadioGroup.prototype.onClick = function(listener) {
	var i;
	
	for (i=0; i<this.buttons.length; i++) {
		addInputElementListener(this.buttons[i].input.firstChild,'click',listener);
	}
};
/**
 * Appends the radio button group and any label to a DOM node
 * @param {Object} parent The DOM element to which this is added
 */
InputElementRadioGroup.prototype.appendTo = function(parent) {
	var i;
	if (this.labelObj.label) {
		if (this.labelObj.align === 'l') {
			parent.appendChild(this.labelObj.label);
			for (i=0; i<this.buttons.length; i++) {
				this.buttons[i].appendTo(parent);
			}
		} else if (this.LabelObj.align === 'r') {
			for (i=0; i<this.buttons.length; i++) {
				this.buttons[i].appendTo(parent);
			}
			parent.appendChild(this.labelObj.label);
		} else {
			for (i=0; i<this.buttons.length; i++) {
				this.buttons[i].appendTo(parent);
			}
		}
	} else {
		for (i=0; i<this.buttons.length; i++) {
			this.buttons[i].appendTo(parent);
		}
	}
};

// These objects are not input types, but look much like them

/**
 * @class A textarea object, not a true input but very similar.
 * @param {string} tag The name and id of this object.
 * @example myComments = new InputElementTextarea('taComments');
 */
function InputElementTextarea(tag) {
	this.input = document.createElement('textarea');
	this.input.setAttribute('id',tag);
	this.input.setAttribute('name',tag);
	this.labelObj = new ElementLabel();	// use composition here, since not derived
}
/**
 * Since this does not have a corresponding input, have an explicit method
 */
InputElementTextarea.prototype.getGUI= function() { return "textarea"; };
/**
 * Sets the text for the associated label.
 * @param {string} text The label for this object.
 */
InputElementTextarea.prototype.setLabel = function(text) {
	this.labelObj.setLabel(text);
};
/**
 * Sets the text alignment for the associated label. Default is left.
 * @param {string} text Should be either 'left' or 'right', depending on where you want the label.
 */
InputElementTextarea.prototype.setTextAlign = function(text) {
	this.labelObj.setTextAlign(text);
};
/**
 * Sets the size of the textarea
 * @param {number} width The width of the textarea in columns
 * @param {number} height The height of the textarea in rows
 */
InputElementTextarea.prototype.setSize = function(width,height) {
	this.input.setAttribute('cols',width);
	this.input.setAttribute('rows',height);
};
/**
 * Appends the textarea and any label to a DOM node
 * @param {Object} parent The DOM element to which this is added
 */
InputElementTextarea.prototype.appendTo = function(parent) {
	if (this.labelObj.label) {
		if (this.labelObj.align === 'l') {
			parent.appendChild(this.labelObj.label);
			parent.appendChild(this.input);
		} else if (this.labelObj.align === 'r') {
			parent.appendChild(this.input);
			parent.appendChild(this.labelObj.label);
		} else {
			parent.appendChild(this.input);
		}
	} else {
		parent.appendChild(this.input);
	}
};
/**
 * Attaches an onchange handler to the object
 * @param {function} listener The event listener to add
 */
InputElementTextarea.prototype.onChange = function(listener) { addInputElementListener(this.input,'change',listener); };
/**
 * Sets the text to be displayed when a user hovers the mouse over the textarea.
 * @param {string} text The display text
 */
InputElementTextarea.prototype.setTooltip = function(text) { this.input.setAttribute("title",text); };
/**
 * Sets the text to be displayed in the textarea.
 * @param {string} text The display text
 */
InputElementTextarea.prototype.setValue = function(text) { this.input.value = text; };
/**
 * Retrieves the text currently displayed in the textarea.
 * @returns {string} The display text
 */
InputElementTextarea.prototype.getValue = function() { return this.input.value; };
/**
 * Disables the Textarea object
 */
InputElementTextarea.prototype.disable = function() { this.input.disabled = true; };
/**
 * Enables the Textarea object
 */
InputElementTextarea.prototype.enable = function() { this.input.disabled = false; };/**
 * @class A drop-down-list object, not a true input but very similar.
 * @constructor
 * @param {string} tag The name and id of the SELECT for this object.
 * @example languageDDL = new InputElementDDL('ddlLanguage');
 */
function InputElementDDL(tag) {
	this.input = document.createElement('select');	// create the DOM select element
	this.input.setAttribute('id',tag);		
	this.input.setAttribute('name',tag);		
	this.labelObj = new ElementLabel();	// use composition here, since not derived
	this.multiple = false;	// assume not
}
InputElementDDL.prototype.rows = 0;		// row count of list
/**
 * Since this does not have a corresponding input, have an explicit method
 */
InputElementDDL.prototype.getGUI = function() { return "ddl"; };
/**
 * Sets the text for the associated label.
 * @param {string} text The label for this object.
 */
InputElementDDL.prototype.setLabel = function(text) {
	this.labelObj.setLabel(text);
};
/**
 * Sets the text alignment for the associated label. Default is left.
 * @param {string} text Should be either 'left' or 'right', depending on where you want the label.
 */
InputElementDDL.prototype.setTextAlign = function(text) {
	this.labelObj.setTextAlign(text);
};
/**
 * Appends the drop-down-list and any label to a DOM node
 * @param {Object} parent The DOM element to which this is added
 */
InputElementDDL.prototype.appendTo = function(parent) {
	if (this.labelObj.label) {
		if (this.labelObj.align === 'l') {
			parent.appendChild(this.labelObj.label);
			parent.appendChild(this.input);
		} else if (this.labelObj.align === 'r') {
			parent.appendChild(this.input);
			parent.appendChild(this.labelObj.label);
		} else {
			parent.appendChild(this.input);
		}
	} else {
		parent.appendChild(this.input);
	}
};
InputElementDDL.prototype.onChange = function(listener) { addInputElementListener(this.input,'change',listener); };
/**
 * Add an HTML attribute to the object, in this case to the SELECT element. If the attribute is "multiple", remember it for changed behavior.
 * @param {string} name The name of the attribute
 * @param {string} value The value of the attribute
 * @example mySelect.setAttribute("multiple","true");
 */
InputElementDDL.prototype.setAttribute = function(name,value) {
	if (name === "multiple") {
		if (value === "true" || value === true) {
			this.multiple = true;
		} else {
			this.multiple = false;
		}
	}
	this.input.setAttribute(name,value);
};
/**
 * Adds a single option to a drop-down-list. Uses the standard JavaScript Option class, so
 * takes the same argument list. If no value is specified, the text argument is used as the value.
 * @example languageTable.addOption('French','FR');
 */
InputElementDDL.prototype.addOption = function(txt,val,def,sel) {
	var value;
	if (val) {	// if no value specified, use the text
		value = val;
	} else {
		value = txt;
	}
	var opt = new Option(txt,value,def,sel);
	this.input.options[this.rows] = opt;
	this.rows++;
};
/**
 * Adds a list of strings to a drop-down-list as options. These will have no specified values.
 * @param {string[]} list An array of strings to become list options
 * @example languageTable.addOptionList(myList);
 */
InputElementDDL.prototype.addOptionList = function(list) {
	var i;
	for (i=0; i<list.length; i++) {
		this.addOption(list[i]);
	}
};
/**
 * Return an object representing an option specified by its index
 * @param {number} index The index of the requested option
 * @returns {object} An object containing the value and text of the requested option or null
 */
InputElementDDL.prototype.getOption = function(index) {
	if (index < this.rows) {
		var optionObj = {
			value: this.input.options[index].value,
			text: this.input.options[index].text
		};
		return optionObj;
	}
	return null;	// bad index
};
/**
 * Sets the selected index of a drop-down list. Less meaningful if it is a multi-select.
 * @param {number} ix The index to select
 */
InputElementDDL.prototype.select = function(ix) { this.input.selectedIndex = ix; };
/**
 * Return the index of the currently selected row. Follows usual rules for HTML selectedIndex.
 * @returns {number} Index corresponding to the selected (or first selected) row.
 */
InputElementDDL.prototype.getSelectedRow = function() { return this.input.selectedIndex; };
/**
 * Return the value of the currently selected row, or null if none selected. DEPRECATED: use getValue() instead.
 * @returns {number} Value corresponding to the selected (or first selected) row.
 */
InputElementDDL.prototype.getSelectedValue = function() {	
	if (this.input.selectedIndex >= 0) { return this.input.options[this.input.selectedIndex].value; }
	return null;
};
/**
 * Return the value of the currently selected row, or null if none selected.
 * @returns {String|null} Value corresponding to the selected (or first selected) row.
 */
InputElementDDL.prototype.getValue = function() {	
	if (this.input.selectedIndex >= 0) { return this.input.options[this.input.selectedIndex].value; }
	return null;
};
/**
 * Return the number of options in the list.
 * @returns {number} The option count.
 */
InputElementDDL.prototype.getRowCount = function() { return this.rows; };
/**
 * Causes the option whose value is specified to become the currently selected option
 * @param {string} find The value of the option to make selected
 * @example languageTable.selectByValue('French');
 */
InputElementDDL.prototype.selectByValue = function(find) {
	var i;
	
	for (i=0; i<this.rows; i++) {
		if (this.input.options[i].value === find) {
			this.input.selectedIndex = i;
			return;
		}
	}
};
/**
 * Disables the DDL object
 */
InputElementDDL.prototype.disable = function() { this.input.disabled = true; };
/**
 * Enables the DDL object
 */
InputElementDDL.prototype.enable = function() { this.input.disabled = false; };