app/js-yui/textboxList.js
YAHOO.namespace("lacuna");
//only load this once
if (typeof YAHOO.lacuna.TextboxList == "undefined" || !YAHOO.lacuna.TextboxList) {
// enclose everything in an anonymous function ...
(function () {
// ... so that variables that you declare inside are local to the function and invisible outside
var Util = YAHOO.util,
Dom = Util.Dom,
Event = Util.Event,
Lang = YAHOO.lang,
Lib = YAHOO.lacuna.Library,
CSS_PREFIX = "TextboxList";
var TBL = function(elInput, oDataSource, oConfigs) {
if(!oConfigs){ oConfigs = {}; }
oConfigs.useShadow = oConfigs.useShadow || false;
oConfigs.useIFrame = oConfigs.useIFrame || true;
oConfigs.animVert = oConfigs.animVert || false;
oConfigs.minQueryLength = oConfigs.minQueryLength || 0; //if null set to zero so we can submit blank request for dropdown display.
oConfigs.resultTypeList = oConfigs.resultTypeList || false; //if false use object literal insted of array in formatResults first arg
oConfigs.formatResultLabelKey = oConfigs.formatResultLabelKey || oDataSource.responseSchema.fields[0].key || oDataSource.responseSchema.fields[0];
oConfigs.formatResultColumnKeys = oConfigs.formatResultColumnKeys || [(oDataSource.responseSchema.fields[0].key || oDataSource.responseSchema.fields[0])]; //used to show correct columns in drop down
oConfigs._pageNum = 1; //private var to remember what page we're on to use the "Next" option
// Validate input element and get idPrefix from it's id
var idPrefix;
if(Dom.inDocument(elInput)) {
if(YAHOO.lang.isString(elInput)) {
idPrefix = elInput;
elInput = document.getElementById(elInput); //get dom object
}
else {
idPrefix = elInput.id;
}
}
else {
return;
}
//setup dom structure
//Dom.setStyle(elInput, "display", "none"); //hide starting input and use it for storage and form submits
var tblInput = Dom.get(elInput), //document.createElement("input"),
tblContainer = document.createElement("div"),
tblAC = document.createElement("div"),
ddIcon, tblListContainer, tblList, tblListInputLine, tblIndicator;
//set needed id's
tblContainer.id = idPrefix + "Tbl"; //TextboxList
//tblInput.id = idPrefix + "TblInput"; //TextboxListInput
tblAC.id = idPrefix + "TblAutoComplete"; //TextboxListAutoComplete
//append to dom and keep references
tblContainer = Dom.insertAfter(tblContainer, tblInput); //insert our container after the input passed in
Dom.addClass(tblContainer, CSS_PREFIX); //add the control class to the Container
//remove elInput from dom so we can reinsert it in our structure
tblInput = tblInput.parentNode.removeChild(tblInput);
//check if we're single or multi select
if(oConfigs.multiSelect) {
tblListContainer = document.createElement("div");
tblList = document.createElement("ul");
tblListInputLine = document.createElement("li");
//Dom.addClass(tblContainer, CSS_PREFIX + "Multi"); //add the control class to the Container
tblListContainer = tblContainer.appendChild(tblListContainer); //append our list to our textbox container
Dom.addClass(tblListContainer, CSS_PREFIX + "Container"); //add the class to the List Container
tblList = tblListContainer.appendChild(tblList); //append our list to our container
Dom.addClass(tblList, CSS_PREFIX + "Bits"); //add the control class to the List
tblListInputLine = tblList.appendChild(tblListInputLine); //append the LI to the list
Dom.addClass(tblListInputLine, CSS_PREFIX + "Bit"); //add the control class to the LI
tblInput = tblListInputLine.appendChild(tblInput); //append our input to the LI
Dom.addClass(tblInput, CSS_PREFIX + "Input"); //add the control class to the Input
}
else {
tblInput = tblContainer.appendChild(tblInput); //append our input to the LI
Dom.addClass(tblInput, CSS_PREFIX + "InputSingle"); //add the control class to the Input
}
if(!oConfigs.multiSelect && oConfigs.minQueryLength == 0){
ddIcon = document.createElement("div");
ddIcon = Dom.insertAfter(ddIcon, tblInput);
Dom.addClass(ddIcon, CSS_PREFIX + "Icon");
}
if(oConfigs.useIndicator) {
//create indicator span for image next to input control
tblIndicator = document.createElement("span");
tblIndicator.innerHTML = ['<img src="',Lib.AssetUrl,'ui/indicator.gif" alt="Loading..." />'].join('');
Dom.setStyle(tblIndicator,"display","none");
Dom.addClass(tblIndicator, CSS_PREFIX + "Indicator");
if(oConfigs.multiSelect) {
tblIndicator = Dom.insertAfter(tblIndicator, tblInput);
}
else {
Dom.addClass(tblIndicator, CSS_PREFIX + "IndicatorSingle");
tblIndicator = Dom.insertBefore(tblIndicator, tblInput);
}
}
tblAC = tblContainer.appendChild(tblAC); //append our AutoComplete div to our container
Dom.addClass(tblAC, CSS_PREFIX + "AutoComplete"); //add the control class to the AutoComplete
//call AutoComplete's constructor
TBL.superclass.constructor.call(this, tblInput, tblAC, oDataSource, oConfigs);
//setup extra events
this.dirtyEvent = new Util.CustomEvent("dirty", this);
this.selectSingleEvent = new Util.CustomEvent("selectSingle", this);
this.beforeDeleteEvent = new Util.CustomEvent("beforeDelete", this);
//assign private vars
this._elTblListContainer = tblListContainer;
this._elTblContainer = tblContainer;
this._elTblList = tblList;
this._elTblIndicator = tblIndicator;
this._elTblIcon = ddIcon;
this._elTblListInputLine = (this.multiSelect ? tblListInputLine : undefined);
this._oTblSelections = {}; //remember what our selections are
this._oTblSingleSelection = null;
this._sOrigSingleSelection = "";
this._aOrigSelections = []; //remember what our original selections were for dirty check
//setup click events if min query is 0
var oSelf = this;
if(this.minQueryLength == 0) {
if(!this.multiSelect) {
Event.addListener(ddIcon,"click",oSelf._onListTextboxClick,oSelf);
}
Event.addListener(tblInput,"click", oSelf._onListTextboxClick, oSelf);
}
//correctly hide the container when mousing out
var mouseEnter = function(){
Event.on(this._elContent, "mouseleave", mouseLeave, this, true);
},
mouseLeave = function() {
Event.removeListener(this._elContent, "mouseleave", mouseLeave);
this._toggleContainer(false);
};
Event.on(this._elContent,"mouseenter", mouseEnter, oSelf, true);
//always add this listener
Event.addListener(tblListContainer,"click",oSelf._onListContainerClick,oSelf);
//if we're using indicator add events to hide and show it
if(this.useIndicator) {
this.dataRequestEvent.subscribe(function(oSelf, sQuery, oRequest) {
this.showIndicator();
});
this.dataErrorEvent.subscribe(function(oSelf, sQuery) {
this.hideIndicator();
});
this.dataReturnEvent.subscribe(function(oSelf, sQuery, oRequest) {
this.hideIndicator();
});
}
};
// now we are actually doing the inheritance part.
Lang.extend(TBL, YAHOO.widget.AutoComplete, {
// the third argument to extend are extra prototypes for the new function
formatResult : function(oResultData, sQuery, sResultMatch) {
var sKey = sResultMatch, // the entire result key which is the "name" param in the result set
sKeyQuery = sKey.substr(0, sQuery.length), // the query itself
sKeyRemainder = sKey.substr(sQuery.length), // the rest of the result
aMarkup = [],
val, key, col = 1;
for(var k = 0; k < this.formatResultColumnKeys.length; k++) {
key = this.formatResultColumnKeys[k];
aMarkup[aMarkup.length] = "<div class='";
aMarkup[aMarkup.length] = CSS_PREFIX;
aMarkup[aMarkup.length] = "Column ";
aMarkup[aMarkup.length] = CSS_PREFIX;
aMarkup[aMarkup.length] = "ColNum";
aMarkup[aMarkup.length] = col;
aMarkup[aMarkup.length] = "'>";
if(oResultData[key]) {
val = oResultData[key];
aMarkup[aMarkup.length] = val;
}
aMarkup[aMarkup.length] = "</div>";
col++;
}
return (aMarkup.join(""));
},
doBeforeExpandContainer : function(oTextbox, oContainer, sQuery, aResults) {
var pos = Dom.getXY(oTextbox);
pos[1] += Dom.get(oTextbox).offsetHeight + 2;
Dom.setXY(oContainer,pos);
return true;
},
/**
* Overload destroy to get ride of our dirtyEvent
*/
destroy : function() {
// call the AutoComplete destroy
TBL.superclass.destroy.call(this);
this.dirtyEvent.unsubscribeAll();
this.selectSingleEvent.unsubscribeAll();
},
/**
* Return key/value pairs of selected items, if multi-selct. "key" will be the selected item formatResultLabelKey. value will be a "bit"
* For single select will return single data item
*/
Selections : function() {
if(this.multiSelect) {
var arr = [];
for(var key in this._oTblSelections){
if(this._oTblSelections.hasOwnProperty(key)){
arr.push(this._oTblSelections[key]._value);
}
}
return arr;
}
else {
return this._oTblSingleSelection.Value;
}
},
/**
* items should be an array of objects with the same schema as the datasource.
*/
SelectItems : function(items) {
//clear prior selections
this._sPastSelections = "";
this.ResetSelections();
this._dirty = null;
//loop and add
for(var i = 0; i < items.length; i++) {
var oData = this._createDataObject(items[i]);
if(oData.Value) {
var bit = this._createBit(oData);
if(bit) {
this._aOrigSelections.push(oData.Value);
this._sPastSelections = bit._value;
this._oTblSelections[bit._value] = bit;
}
}
}
},
/**
* Reset selections
*/
ResetSelections : function() {
if(this.multiSelect){
this._oTblSelections = {};
this._aOrigSelections = [];
var list = this._elTblList;
//remove all children
while (list.firstChild) {
//if it doesn't have the bitbox class it's the input field so break out of the while loop because we're done
if(!Dom.hasClass(list.firstChild, CSS_PREFIX + "BitBox")) {
break;
}
else {
list.removeChild(list.firstChild);
}
}
}
else {
this._oTblSingleSelection = null;
this._sOrigSingleSelection = "";
this._elTextbox.value = "";
}
},
/**
* _singleRequest getter
*/
IsSingleRequest : function() {
return this._singleRequest;
},
handleResponseSingle : function(sQuery, oResponse, oPayload) {
if((this instanceof YAHOO.widget.AutoComplete) && this._sName) {
//focus textbox
this._focus();
//container won't show since it's a single request
this._populateList(sQuery, oResponse, oPayload);
//get all list items
var elListItem, allListItemEls = this._elList.childNodes,
queue = (oPayload && oPayload.queue) ? oPayload.queue : null;
if(queue) {
for(var i=0; i<allListItemEls.length; i++) {
elListItem = allListItemEls[i];
if(elListItem._oResultData && queue.indexOf(String(elListItem._oResultData.Id)) >= 0) {
this._selectItem(elListItem);
}
}
}
else {
//get first
elListItem = allListItemEls[0];
//select it
this._selectItem(elListItem);
}
//remove single response flag
this._singleRequest = null;
this.dataSource._singleRequest = null;
//if indicator hide it
if(this.useIndicator) {
this.hideIndicator();
}
//done with single select
this.selectSingleEvent.fire();
}
},
/**
* Selects single value. Must override generateRequest to properly handle key unless the key is the what is the normally queried text
*/
Select : function(key) {
if(key) {
// Reset focus for a new interaction
this._bFocused = null;
//set single request flags
this._singleRequest = true;
this.dataSource._singleRequest = true;
//get request
var sRequest = this.generateRequest(key);
//if indicator show it
if(this.useIndicator) {
this.showIndicator();
}
//send
this.dataSource.sendRequest(sRequest, {
success : this.handleResponseSingle,
failure : this.handleResponseSingle,
scope : this,
argument: {
query: key
}
});
}
},
Queue : function(keys) {
if(Lang.isArray(keys) && keys.length > 0) {
// Reset focus for a new interaction
this._bFocused = null;
//set single request flags
this._singleRequest = true;
this.dataSource._singleRequest = true;
//get request
var sRequest = this.generateRequest("");
//if indicator show it
if(this.useIndicator) {
this.showIndicator();
}
//send
this.dataSource.sendRequest(sRequest, {
success : this.handleResponseSingle,
failure : this.handleResponseSingle,
scope : this,
argument: {
query: "",
queue: keys
}
});
}
},
/**
* overload populateList to handle single Select and use formatResultLabelKey instead of the first schema field
*/
_populateList : function(sQuery, oResponse, oPayload) {
// Clear previous timeout
if(this._nTypeAheadDelayID != -1) {
clearTimeout(this._nTypeAheadDelayID);
}
sQuery = (oPayload && oPayload.query) ? oPayload.query : sQuery;
// Pass data through abstract method for any transformations
var ok = this.doBeforeLoadData(sQuery, oResponse, oPayload);
// Data is ok
if(ok && !oResponse.error) {
this.dataReturnEvent.fire(this, sQuery, oResponse.results);
// Continue only if instance is still focused (i.e., user hasn't already moved on)
// Null indicates initialized state, which is ok too
//if(this._bFocused || (this._bFocused === null)) {
//TODO: is this still necessary?
/*var isOpera = (YAHOO.env.ua.opera);
var contentStyle = this._elContent.style;
contentStyle.width = (!isOpera) ? null : "";
contentStyle.height = (!isOpera) ? null : "";*/
// Store state for this interaction
var sCurQuery = decodeURIComponent(sQuery);
this._sCurQuery = sCurQuery;
this._bItemSelected = false;
var allResults = oResponse.results,
nItemsToShow = Math.min(allResults.length,this.maxResultsDisplayed),
sMatchKey = (this.dataSource.responseSchema.fields) ?
this.formatResultLabelKey : 0;
if(nItemsToShow > 0) {
// Make sure container and helpers are ready to go
if(!this._elList || (this._elList.childNodes.length < nItemsToShow)) {
this._initListEl();
}
this._initContainerHelperEls();
var allListItemEls = this._elList.childNodes;
// Fill items with data from the bottom up
for(var i = nItemsToShow-1; i >= 0; i--) {
var elListItem = allListItemEls[i],
oResult = allResults[i];
// Backward compatibility
if(this.resultTypeList) {
// Results need to be converted back to an array
var aResult = [];
// Match key is first
aResult[0] = (YAHOO.lang.isString(oResult)) ? oResult : oResult[sMatchKey] || oResult[this.key];
// Add additional data to the result array
var fields = this.dataSource.responseSchema.fields;
if(YAHOO.lang.isArray(fields) && (fields.length > 1)) {
for(var k=1, len=fields.length; k<len; k++) {
aResult[aResult.length] = oResult[fields[k].key || fields[k]];
}
}
// No specific fields defined, so pass along entire data object
else {
// Already an array
if(YAHOO.lang.isArray(oResult)) {
aResult = oResult;
}
// Simple string
else if(YAHOO.lang.isString(oResult)) {
aResult = [oResult];
}
// Object
else {
aResult[1] = oResult;
}
}
oResult = aResult;
}
// The matching value, including backward compatibility for array format and safety net
elListItem._sResultMatch = (YAHOO.lang.isString(oResult)) ? oResult : (YAHOO.lang.isArray(oResult)) ? oResult[0] : (oResult[sMatchKey] || "");
elListItem._oResultData = oResult; // Additional data
elListItem.innerHTML = this.formatResult(oResult, sCurQuery, elListItem._sResultMatch);
elListItem.style.display = "";
}
// Clear out extraneous items
if(nItemsToShow < allListItemEls.length) {
var extraListItem;
for(var j = allListItemEls.length-1; j >= nItemsToShow; j--) {
extraListItem = allListItemEls[j];
extraListItem.style.display = "none";
}
}
this._nDisplayedItems = nItemsToShow;
this.containerPopulateEvent.fire(this, sQuery, allResults);
// Highlight the first item
if(this.autoHighlight) {
var elFirstListItem = this._elList.firstChild;
this._toggleHighlight(elFirstListItem,"to");
this.itemArrowToEvent.fire(this, elFirstListItem);
this._typeAhead(elFirstListItem,sQuery);
}
// Unhighlight any previous time
else {
this._toggleHighlight(this._elCurListItem,"from");
}
// If not a single request toggle container
if(!this._singleRequest) {
// Expand the container
ok = this.doBeforeExpandContainer(this._elTextbox, this._elContainer, sQuery, allResults);
this._toggleContainer(ok);
}
}
else {
this._toggleContainer(false);
}
return;
}
// Error
else {
this.dataErrorEvent.fire(this, sQuery);
}
},
/**
* Enabled and disable the field
*/
disable : function() {
this.getInputEl().disabled = true;
this.disabled = true;
Dom.addClass(this.getInputEl(), CSS_PREFIX + "Disabled");
Dom.addClass(this._elTblContainer, CSS_PREFIX + "Disabled");
Dom.addClass(this._elTblListContainer, CSS_PREFIX + "Disabled");
for(var bitKey in this._oTblSelections) {
var bit = this._oTblSelections[bitKey];
Dom.addClass(bit, CSS_PREFIX + "Disabled");
}
if(this._elTblIcon) {
Dom.setStyle(this._elTblIcon, "display", "none");
}
},
enable : function() {
this.getInputEl().disabled = false;
this.disabled = undefined;
Dom.removeClass(this.getInputEl(), CSS_PREFIX + "Disabled");
Dom.removeClass(this._elTblContainer, CSS_PREFIX + "Disabled");
Dom.removeClass(this._elTblListContainer, CSS_PREFIX + "Disabled");
for(var bitKey in this._oTblSelections) {
var bit = this._oTblSelections[bitKey];
Dom.removeClass(bit, CSS_PREFIX + "Disabled");
}
if(this._elTblIcon) {
Dom.setStyle(this._elTblIcon, "display", "");
}
},
/**
* validity handlers
**/
setValid : function(isValid) {
this._isValid = isValid;
if (isValid) {
Dom.removeClass(this._elTblContainer, CSS_PREFIX + "Error");
} else {
Dom.addClass(this._elTblContainer, CSS_PREFIX + "Error");
}
},
isValid : function(){
return this._isValid;
},
/**
* show and hide animated indicator gif
*/
showIndicator : function() {
Dom.setStyle(this._elTblIndicator, "display", "");
},
hideIndicator : function() {
Dom.setStyle(this._elTblIndicator, "display", "none");
},
/**
* private functions to handle selection update and bit creation for multi select and pass through for single select
*/
_createDataObject : function(data) {
var oData = {};
oData.Object = data; //store actual data structure for possible later use
oData.Value = data ? data[this.formatResultLabelKey] : null;
return oData;
},
_createBit : function(oData) {
var elTblListInputLine = this._elTblListInputLine,
oSelf = this;
var bit;
//add bit if it doesn't exist already
if(!this._oTblSelections[oData.Value]) {
bit = document.createElement("li");
bit.Object = oData.Object; //store actual data structure for possible later use
bit._value = oData.Value;
var bitText = document.createTextNode(bit._value);
var bitDelete = document.createElement("a");
bitDelete.href = "#";
bit = Dom.insertBefore(bit, elTblListInputLine);
Event.addListener(bit, "click", oSelf._onListRemoveBitClick, oSelf);
Dom.addClass(bit, CSS_PREFIX + "Bit");
Dom.addClass(bit, CSS_PREFIX + "BitBox");
//add text
bit.appendChild(bitText);
//add delete
bitDelete = bit.appendChild(bitDelete);
Dom.addClass(bitDelete, CSS_PREFIX + "BitBoxDelete");
}
return bit;
},
_updateValue : function(elListItem) {
if(this.multiSelect){
if(!this.suppressInputUpdate) {
var elTextbox = this._elTextbox,
data = elListItem._oResultData;
// clear input field
elTextbox.value = "";
//create bit from data
var oData = this._createDataObject(data);
var bit = this._createBit(oData);
return bit;
}
}
else {
TBL.superclass._updateValue.call(this, elListItem);
}
},
_selectItem : function(elListItem) {
if(elListItem && elListItem !== null && elListItem._oResultData) {
if(this.multiSelect){
this._bItemSelected = true;
var newBit = this._updateValue(elListItem);
this._clearInterval();
if(newBit){
var newVal = newBit._value;
this._sPastSelections = newVal;
this._oTblSelections[newVal] = newBit;
this._updateDirty();
this.itemSelectEvent.fire(this, elListItem, elListItem._oResultData);
}
this._toggleContainer(false);
}
else {
var oData = this._createDataObject(elListItem._oResultData);
this._oTblSingleSelection = oData;
TBL.superclass._selectItem.call(this, elListItem);
this._updateDirty();
}
//remove focus so enter key doesn't submit
this._elTextbox.blur();
}
},
//click event catch
_onListTextboxClick : function(v, oSelf){
if(!this.disabled) {
Event.stopEvent(v); //stop event from bubbling to container
oSelf._sendQuery(""); //must pass empty string as query
}
},
_onListContainerClick : function(v, oSelf){
if(!this.disabled) {
if(oSelf.minQueryLength == 0) {
oSelf._elTextbox.focus();
oSelf._sendQuery("");
}
else {
oSelf._elTextbox.focus();
}
}
},
_onListRemoveBitClick : function(v, oSelf){
if(oSelf._elTblList && !oSelf.disabled) {
var eventTarget = Event.getTarget(v); //stop event from bubbling to container
Event.stopEvent(v);
//if this is a bit...
if(Dom.hasClass(eventTarget, "TextboxListBitBoxDelete")) {
var target = eventTarget.parentNode; //get target should return the delete link so we want it's parent
if(target) {
//double check delete
if(oSelf.beforeDeleteEvent.fire(target)) {
delete oSelf._oTblSelections[target._value];
oSelf._elTblList.removeChild(target);
oSelf._updateDirty();
}
}
}
}
},
_updateDirty : function() {
//only update dirty if it's not a select
if(!this._singleRequest) {
var isDirty;
if(this.multiSelect){
var cur = this._oTblSelections,
orig = this._aOrigSelections;
for(var s = 0; s < orig.length; s++) {
if(!cur[orig[s]]) {
isDirty = true;
break;
}
}
//if we're not dirty yet loop current selection and count to see if we have more selected than original
if(!isDirty) {
var curNum = 0;
for(var key in cur) {
//if the key actually has a value count it
if(cur[key]) {
curNum++;
}
}
if(curNum != orig.length) {
isDirty = true;
}
}
}
else {
isDirty = this._sOrigSingleSelection != this._oTblSingleSelection.Value;
}
//if it changes reset validity
if(isDirty) {
this.setValid(true);
}
this.dirtyEvent.fire(isDirty);
}
}
});
// Copy static members to class
Lang.augmentObject(TBL, YAHOO.widget.AutoComplete);
Lang.augmentObject(TBL, {
dirtyEvent : null,
selectSingleEvent : null,
beforeDeleteEvent : null
});
//assign to global
YAHOO.lacuna.TextboxList = TBL;
})();
// Important to register it
YAHOO.register("textboxList", YAHOO.lacuna.TextboxList, {version: "1.0.0", build: "1"});
}