/**
 * @fileOverview
 * Embed a SMILE Pathway search widget on a web page
 * @version 1.3
 *
 */
/*
Changelog
20010203 dbluestein method setConfig() deprecated. Doesn't seem to be a need for configuration
                       after initialization, so configure the widget when calling initWidget() instead.
20100202 dbluestein New skin (new HTML markup; scroll up/down buttons & keyboard 
                       arrow-key scrolling controls), resize results list to fit
                       properly when persistent search bar is showing
20100111 dbluestein Added showPersistentSearch config option and rudimentary
                       form to re-do query with an arbitrary search term to 
                       whatever query is already set up for the page
20091027 dbluestein Updated URLs to smile non-beta site
20091013 dbluestein Updated URLs to SMILE page with node-id instead of dds id
20090925 dbluestein Version bump to 1.1. API Incompatibility with previous versions (1.0.x):
		       initWidget now takes a configuration object as it's 3rd
		       parameter instead of the boolean 'deferSearch'. deferSearch is now a member 
		       of the configurable options.
20090924 dbluestein added getVersion() function
                    added setConfig() function. Call setConfig() before initWidget if initial
		        search is not deferred
20090923 dbluestein updated for new NCS images
*/

if( window.smileWidget == undefined )
{
	/** @namespace */
	var smileWidget = new function()
	{
		/** @private name of this widget (for use in html element names/ids */
		var widgetName = 'smileWidget';
		/** @private version number */
		var widgetVersion = '1.3';
		
		/** 
		 * @namespace
		 * (JavaScript object literal) configuration options for smileWidget
		 * @example
		 * // this example demonstrates setting a timeout value and two
		 * // ways of setting callback functions
		 * var timeout = 500;
		 * function noresults() {
		 * 	alert('Sorry that search returned no results.');
		 * }
		 * var conf = {
		 *	queryTimeout:timeout,
		 *	queryTimeoutCallback:function(){
		 *		alert( 'No data received from server after '+timeout+' millisec' );
		 *	},
		 *	noResultsCallback:noresults,
		 * }
		 * smileWidget.initWidget(null, {institution:{'The Exploratorium'}, conf );
		 */
		var config = {
			/** 
			 * how long to wait for response from server, in milliseconds (0 means wait forever)
			 * NOTE that the widget will not cancel it's attempt to load data even with a 
			 * timeout specified, so results may still load.
			 * @type Int
			 * @default 0
			 */
			queryTimeout: 0,
			/**
			 * callback function to call if timeout is reached with no response from server
			 * NOTE that the widget will not cancel it's attempt to load data even with a 
			 * timeout specified, so results may still load.
			 * @type Function
			 * @default null
			 */
			queryTimeoutCallback: null,
			/**
			 * callback function to call if empty result set is received from server
			 * @type Function
			 * @default null
			 */
			noResultsCallback: null,
			/**
			 * html content to be used when no results are returned by a search
			 * @type String
			 * @default 'Your search returned no results.'
			 */
			noResultsHTML: 'Your search returned no results.',
			/**
			 * When false, widget will initiate a search immediately when 
			 * {@link smileWidget.initWidget} is called. When true widget will not
			 * perform a search when {@link smileWidget.initWidget} is called
			 * (then use {@link smileWidget.search} to perform a search).
			 * @type Boolean
			 * @default false
			 */
			deferSearch: false,
			/**
			 * When true, widget will display a persistent search-term entry form
			 * to add any given search term to the pre-configured search
			 * parameters. (If the widget is intialized with a "term" search parameter,
			 * it's value will be pre-filled in the form field)
			 * @type Boolean
			 * @default false
			 * @since 1.2
			 */
			showPersistentSearch: false,
			/**
			 * When true, the widget will start out hidden, and show
			 * itself only when it has retrieved a non-empty result set.
			 * @type Boolean
			 * @default false
			 * @since 1.3
			 */
			 startHidden: false,
			/**
			 * If headerUrl isn't null, the specified URL will be used
			 * in place of the default howtosmile logo header image. The
			 * default header logo is 200x66px. You may need to use 
			 * customized CSS for acceptable results with a different size
			 * image.
			 * @type String
			 * @since 1.3
			 */
			headerUrl: null
		};
		
		/** @private store the timeout resource ID if waiting for a timeout */
		var queryTimeoutId = null;
		
		/** @private url from which data will be loaded */
		var queryUrl = 'http://howtosmile.org/widgetresourcesearch';
		
		/** @private parameters to be used when searching */
		var searchParams = {};
		
		/** @private result of last query */
		var results = null;
		
		/** @private array of result document objects */
		var current_results = [];
		
		/**
		 * @private object to cache paginated result sets so data won't have to 
		 * be retrieved multiple times if user clicks page backwards.
		 */
		var cachedPageData = {};
		
		/** @private save state of single-record view so we know if it needs to be hidden */
		var recordShowing = false;
		/** @private instance of Animator object to use for sliding effects */
		var animator = null;
		
		/** @private distance to move list per scroll interval (pixels) */
		var scrollIncrement = 30;
		/** @private scrolling interval between incrementing position of list (millisec) */
		var scrollInterval = 50;
		/** @private interval ID for list scrolling operation */
		var scrollIntervalId = null;
		
		/**
		 * Return the HTML for the basic widget with no contents
		 */
		function _createWidgetHtml()
		{
			var searchBarHtml = ( config.showPersistentSearch ) ? _createSearchBarHtml() : '';
			return '\n	<div id="' + widgetName + '_container">\n'
			+'		<div classname="hid" class="hid" id="' + widgetName + '_loading"></div>\n'
			+'		<div id="' + widgetName + '_header">\n'
			+'			<div id="' + widgetName + '_header_topStripe"></div>\n'
			+'			<div id="' + widgetName + '_header_logo"></div>\n'
			+'		</div>\n'
			+ searchBarHtml
			+'		<div id="' + widgetName + '_result">\n'
			+'			<div id="' + widgetName + '_resultcount"></div>\n'
			+'			<div id="' + widgetName + '_listbackward" class="' + widgetName + '_scroll smileWidget_scrollbackward"></div>\n'
			+'			<div id="' + widgetName + '_list_holder">\n'
			+'				<div id="' + widgetName + '_list">\n'
			+'					<!-- list contents will be filled in here by script -->\n'
			+'				</div><!-- end _list -->\n'
			+'			</div><!-- end _list_holder -->\n'
			+'			<div id="' + widgetName + '_listforward" class="' + widgetName + '_scroll ' + widgetName + '_scrollforward"></div>\n'
			+'			<div id="' + widgetName + '_pagination"></div>\n'
			+'			<div id="' + widgetName + '_result_record">\n'
			+'			<a id="' + widgetName + '_back" href="javascript:' + widgetName + '.hideRecord();">back to results</a>\n'
			+'			<div id="' + widgetName + '_record_inner"></div>\n'
			+'		</div>\n'
			+'		</div><!-- end _result -->\n'
			+'	</div><!-- end _container -->\n';
		}
		
		function _createSearchBarHtml()
		{
			return '		<div id="' + widgetName + '_search">\n'
			+'			<div id="' + widgetName + '_searchbg1"></div>\n'
			+'			<div id="' + widgetName + '_searchbg2"></div>\n'
			+'			<div id="' + widgetName + '_searchbg3"></div>\n'
			+'			<form id="' + widgetName + '_searchform">\n'
			+'				<input id="' + widgetName + '_searchterm" value="Search" type="text">\n'
			+'				<a id="' + widgetName + '_searchbtn" href="#">&nbsp;</a>\n'
			+'			</form>\n'
			+'		</div>\n';
		}
		
		/**
		 * Activate handlers on search bar form/elements and make search bar html visible
		 */
		function _initSearchBar()
		{
			var form = document.getElementById( widgetName+'_searchform');
			if( form )
			{
				form.onsubmit = _searchFormSubmit;
				var txtArea = document.getElementById( widgetName+'_searchterm');
				if( txtArea )
					txtArea.onfocus = _searchAreaFocused;
				if( searchParams.term )
					txtArea.value = searchParams.term;
				
				document.getElementById( widgetName+'_searchbtn' ).onclick = _searchFormSubmit;
			}
			var elem = document.getElementById( widgetName + '_search' );
			if( elem.style.display != 'block' )
			{
				elem.style.display = 'block';
			}
			
			_fixListHeight();
			_fixSearchInputWidth();
		}
		
		/**
		 * Adjust the width of the _searchterm input element so it will 
		 * properly fill its space, in case a non-standard widget width
		 * is used.
		 */
		function _fixSearchInputWidth()
		{
			if( _fixSearchInputWidth.inputFixIntervalId != undefined )
				clearInterval(_fixSearchInputWidth.inputFixIntervalId);
			var left = document.getElementById( widgetName + '_searchbg1' );
			var right = document.getElementById( widgetName + '_searchbg3' );
			var w = right.offsetLeft - ( left.offsetWidth - left.offsetLeft );
			/* In webkit browsers sometimes the element still has offsetWidth of 0 when
			script gets here (hasn't had its styles computed yet?) so loop back to this 
			function until it's ready */
			if( w <= 0 )
			{
				_fixSearchInputWidth.inputFixIntervalId = setInterval( _fixSearchInputWidth, 1 );
				return;
			}
			var elem = document.getElementById( widgetName + '_searchterm' );
			elem.style.width = w;
		}
		
		/**
		 * Adjust the height of the _listholder div to account for non-default widget height
		 *
		 * _list_holder height = 
		 *		   _container height
		 *		-  _header_topStripe height
		 *		-  _header_logo height
		 *		-  _header_logo bottom border thickness
		 *		-  _resultcount height
		 *		-  _resultcount top padding
		 *		-  _resultcount bottom border thickness
		 *		-  _listbackward height
		 *		-  _listforward height
		 *		-  _pagination height
		 *		- _search (only if search bar is showing)
		 */
		function _fixListholderHeight()
		{
			var holderHeight = document.getElementById(widgetName+'_container').clientHeight
					- document.getElementById(widgetName+'_header_topStripe').offsetHeight
					- document.getElementById(widgetName+'_header_logo').offsetHeight
					- document.getElementById(widgetName+'_resultcount').offsetHeight
					- document.getElementById(widgetName+'_listbackward').offsetHeight
					- document.getElementById(widgetName+'_listforward').offsetHeight
					- document.getElementById(widgetName+'_pagination').offsetHeight;
			var searchbar = document.getElementById(widgetName+'_search');
			if( searchbar )
				holderHeight -= searchbar.offsetHeight;
			var elem = document.getElementById( widgetName + '_list_holder' );
			elem.style.height = holderHeight + 'px';
		}
		
		/**
		 * Adjust the height of the _listholder div so it won't be too tall
		 * when the search bar is visible.
		 */
		function _fixListHeight()
		{
			if( _fixListHeight.listFixIntervalId != undefined )
				clearInterval(_fixListHeight.listFixIntervalId);
			var listHolder = document.getElementById( widgetName + '_list_holder' );
			var listElemHt = listHolder.offsetHeight;
			
			/* In webkit browsers sometimes the element still has offsetHeight of 0 when
			script gets here (hasn't had its styles computed yet?) so loop back to this 
			function until it's ready */
			if( listElemHt == 0 )
			{
				_fixListHeight.listFixIntervalId = setInterval( _fixListHeight, 1 );
				return;
			}
			var searchElemHt = document.getElementById(widgetName + '_search').offsetHeight;
			var listHeight = listElemHt - searchElemHt;
			listHolder.style.height = listHeight + 'px';
		}
		
		/**
		 * Adjust the height of the _result_record div so it won't be too tall
		 * when the search bar is visible.
		 */
		function _fixRecordHeight()
		{
			var recHeight = document.getElementById(widgetName+'_result').offsetHeight;
			document.getElementById( widgetName + '_result_record' ).style.height =  recHeight+'px';
		}
		/**
		 * If there's a custom header image specified, set the CSS to show it instead
		 * of the default howtosmile.org image. 
		 */
		function _initHeaderImage()
		{
			if( config.headerUrl )
			{
				var elem =document.getElementById( widgetName+'_header_logo' ); 
				elem.style.backgroundImage = 'url('+config.headerUrl+')';
			}
		}
		
		/**
		 * Clear out the default text from the search bar input field when field gets focus.
		 */
		function _searchAreaFocused()
		{
			if( this.value == 'Search' )
				this.value = '';
		}
		
		/** Submit handler for search form  */
		function _searchFormSubmit()
		{
			searchParams.term = _getSearchBarTerm();
			_submitSearch();
			return false;
		}
		
		/** Return current value of the search bar text input. */
		function _getSearchBarTerm()
		{
			var txtArea = document.getElementById( widgetName+'_searchterm');
			if( txtArea )
				return txtArea.value;
			else
				return '';
		}
		
		/**
		 * generate a search url and add a script tag to the widget that will
		 * load javascript code from the url
		*/
		function _submitSearch(page)
		{
			if( page===undefined )
				page = 0;
			//alert('page: '+page);
			_clearDisplay();
			_showSpinner();
			
			if( cachedPageData[page] )
			{
				_receivedResults(cachedPageData[page]);
				return false;
			}
			searchParams['pagenumber'] = page;
			searchParams['widget_callback'] = 'dataLoaded';
			var paramStr = _paramsToQueryString();
			var url = queryUrl + '?' + paramStr;
			
			_setScriptTagUrl(url);
			if( config.queryTimeout && config.queryTimeoutCallback )
			{
				queryTimeoutId = setTimeout( config.queryTimeoutCallback, config.queryTimeout );
			}
		}
		
		/**
		 * Add a dynamically created script tag to the widget.
		 * @param url
		 *    the url for the script tag's src attribute
		 */
		function _setScriptTagUrl(url)
		{
			_clearScriptTag();
			var elem = document.getElementById(widgetName+'_container');
			var script = document.createElement('script');
			script.id = widgetName+"_dataScript";
			script.type = 'text/javascript';
			elem.appendChild(script);
			script.src = url;
		}
		
		/** Remove dynamically added script tag (if there is one) from the widget. */
		function _clearScriptTag()
		{
			var elem = document.getElementById(widgetName+'_container');
			var script = document.getElementById(widgetName+'_dataScript');
			if( script!=null )
				elem.removeChild(script);
		}
		
		/**
		 * Convert searchParams object into url get request style parameters.
		 */
		function _paramsToQueryString()
		{
			var params = [];
			for( var p in searchParams )
			{
				// special case for "moreInfo"
				if( p=="moreInfo" )
				{
					if( searchParams[p] instanceof Array )
						params.push( _moreInfoParamArrayToString(searchParams[p]) );
					else if( searchParams[p] instanceof Object )
						params.push( _moreInfoParamToString(searchParams[p]) );
				}
				else if( searchParams[p] instanceof Array )
				{
					params.push( _arrayParamToString(p, searchParams[p]) );
				}
				else
					params.push( p+"="+searchParams[p] );
			}
			return params.join('&');
		}
		
		/**
		 * Generate a get request query string from an array.
		 * @param name
		 *    String, name of query variable to set (square brackets will be
		 *    added)
		 * @param arr
		 *    Array, values to set in the query string
		 * @example
		 * // return value from this example will be
		 * // myVar[]=foo&myVar[]=bar
		 * _arrayParamToString( 'myVar', ['foo','bar'] );
		 */
		function _arrayParamToString(name, arr)
		{
			var params = [];
			for( var i=0; i<arr.length; ++i )
			{
				params.push( name+"[]="+arr[i] );
			}
			return params.join('&');
		}
		
		/**
		 * Return a get variable query string from the "moreInfo" search parameter array.
		 * @param arr
		 *    =moreInfo parameter array
		 * @example
		 * // moreInfo parameter is set as in the following example
		 * moreInfo: [
		 *   {
		 *     field: 'myElement/myNestedElement',
		 *     value: [2,4,6]
		 *   },
	         * ]
		 */
		function _moreInfoParamArrayToString(arr)
		{
			var params = [];
			for( var i=0; i<arr.length; ++i )
			{
				params.push( _moreInfoParamToString(arr[i]) );
			}
			return params.join('&');
		}
		
		/**
		 * Return a get variable query string from an entry in the moreInfo 
		 * search parameter array.
		 * @param obj
		 *    Object, moreInfo entry
		 */
		function _moreInfoParamToString(obj)
		{
			var f = obj.field;
			var fname = "moreInfo[" + f + "]";
			if( !f )
				return '';
			var val = obj.value;
			if( val instanceof Array )
			{
				return _arrayParamToString(fname,val);
			}
			else
			{
				return fname + "=" + val;
			}
		}
		
		/** Clear the results list and hide individual record (if one is showing). */
		function _clearDisplay()
		{
			var list = document.getElementById(widgetName+'_list');
			list.innerHTML = '';
			list.style.top = 0;
			_hideRecord();
		}
		
		/** Show the "loading" spinner graphic. */
		function _showSpinner()
		{
			_removeClass( document.getElementById(widgetName+'_loading') );
		}
		
		/** hide the "loading" spinner graphic. */
		function _hideSpinner()
		{
			_setClass( document.getElementById(widgetName+'_loading'), "hid");
		}
		
		/** Set a search parameter. */
		function _setSearchParam(paramName, value)
		{
			if( value==null )
				delete( searchParams[paramName] );
			else
				searchParams[paramName] = value;
		}
		
		/** set a configuration option */
		function _setConfig(oConf)
		{
			for( var k in oConf )
			{
				if( config.hasOwnProperty(k) )
					config[k] = oConf[k];
			}
		}
		
		/** Initiate processing of results from server. 
		 * Called by dataLoaded callback.
		 */
		function _receivedResults(res)
		{
			if( queryTimeoutId )
			{
				clearTimeout( queryTimeoutId );
				queryTimeoutId = null;
			}
			results = res;
			_hideSpinner();
			cachedPageData[_getCurrentPage()] = results;
			var html;
			if( res.numresults == 0 )
			{
				html = config.noResultsHTML;
				if( config.noResultsCallback )
					config.noResultsCallback();
			}
			else
			{
				html = _processSmileResults(results);
				_showPaginationLinks();
				_showResultCount();
				_showContainer(); // in case container starts out hidden
			}
			document.getElementById(widgetName+'_list').innerHTML =  html;
			// automatically show full record if there's only one
			if( results.numresults==1 )
				_showRecord(0);
			
			_enableScroll('forward');
			_enableScroll('backward');
			_enableKeyScrollEvents();
		}
		
		/** Generate html output from current result set */
		function _processSmileResults(res)
		{
			var docs = res.searchresults;
			current_results = [];
			var output_results = "";
			for (var j=0;j<docs.length;j++)
			{
				var res_id = current_results.push( docs[j] ) - 1;
				output_results += _formatListRecord( res_id, docs[j] );
			}
			return output_results;
		}
		
		/** Display the count of retrieved results. */
		function _showResultCount()
		{
			var numfound = parseInt(results.numresults);
			var resultstart = parseInt(results.offset);
			var startnum = resultstart + 1;
			var endnum = resultstart + parseInt(results.numonpage);
			
			document.getElementById(widgetName+'_resultcount').innerHTML = "Showing "+startnum+"-"+endnum+" of "+numfound+" records";
		}
		
		/**
		 * Generate HTML for "short" record displayed on list of results.
		 * @param id Integer index of record in current_results array
		 * @param doc Object individual record in the searchresults array of the 
		 *        server's response
		 */
		function _formatListRecord(id,doc)
		{
			var rec = doc.record.metadata.smileItem;
			return '		<div class="' + widgetName + '_listrec">\n'
				+'			<div class="' + widgetName + '_titlewrap">\n'
				+ '				<a href="#" onclick="' + widgetName + '.showRecord(' + id + ');return false;" class="' + widgetName + '_title">\n'
				+ rec.activityBasics.title
				+ '\n				</a>\n			</div>\n'
				+'			<img src="' + _getImageUrl(doc) + '" class="' + widgetName + '_thumb"/>\n'
				+'			<div class="' + widgetName + '_listrec_inner2">\n'
				+'			' + _formatStars(doc) + '<br/>\n'
				+'			<a href="#" onclick="' + widgetName + '.showRecord(' + id + ');return false;" class="' + widgetName + '_moreinfo">More Info</a>\n'
				+'			</div>\n'
				+'			<div class="' + widgetName + '_desc">\n'
				+'				' + _shortenText(rec.activityBasics.description, 120) + '...\n'
				+'			</div>\n'
				+'		</div>\n';
		}
		
		/**
		 * Generate HTML for long record display.
		 * @param doc Object
		 */
		function _formatLongRecord(doc)
		{
			var rec = doc.record.metadata.smileItem;
			
			var drupalUrl = _getDetailsUrl(doc)
			var kwdStr = '';
			if( rec.activityBasics.keywords.keyword )
			{
				var kwds = rec.activityBasics.keywords.keyword;
				if( ! kwds instanceof Array )
					kwds = [kwds];
				
				kwdStr = kwds.join(', ');
			}
			
			var title = rec.activityBasics.title;
			if( rec.activityBasics.subtitle )
				title += ': ' + rec.activityBasics.subtitle;
			
			return '			<div class="' + widgetName + '_titlewrap">' + title + '</div>\n'
				+'			<img src="' + _getImageUrl(doc) + '" class="' + widgetName + '_thumb"/>\n'
				+'			<div class="' + widgetName + '_fullrec_inner2">\n'
				+'				<a href="'+drupalUrl+'" class="' + widgetName + '_smiledetails">SMILE Details</a>\n'
				+'				<a href="'+_getResourceUrl(doc)+'" class="' + widgetName + '_goactivity">Go to activity</a>'
				+'\n			</div>\n'
				+'			<div class="' + widgetName + '_desc">\n'
				+'				' + rec.activityBasics.description
				+'\n			</div>\n'
				+'			<div class="'+ widgetName + '_keywords">\n'
				+'				Keywords: ' + kwdStr
				+'\n			</div>\n'
				+'			<div class="' + widgetName + '_comments">\n'
				+'				Comments (' + doc.numcomments + ') ' + _formatStars(doc)
				+'\n			</div>\n';
		}
		
		/** Return HTML for the star-ratings. */
		function _formatStars(doc)
		{
			var rating = doc.rating;
			var maxstarwidth = 75;
			var starwidth = Math.ceil(maxstarwidth * rating / 100);
			return '<div class="stardiv"><ul class="star" title="Rating"><li class="curr" style="width:' + starwidth + 'px;"/></ul></div>';
		}
		
		/**
		 * get URL to SMILE details page for a record
		 */
		function _getDetailsUrl(doc)
		{
			return 'http://howtosmile.org/record/' + doc.nodeId;
		}
		
		/** Get URL to view resource of a record (with smilebar). */
		function _getResourceUrl(doc)
		{
			return 'http://howtosmile.org/resource/viewresource/' + doc.nodeId;
		}
		
		/** 
		 * Get URL of thumbnail image.
		 * @param doc
		 *    Object Literal, metadata for the record to get the thumbnail path from
		 */
		function _getImageUrl(doc)
		{
			var result = doc.record.metadata.smileItem;
			var doc_id = result.activityBasics.recordID;
			var col_id = doc.record.head.collection.key;
			var has_image = (result.activityBasics.images!=undefined && result.activityBasics.images.primaryImage!=undefined);
			var doc_image = '';
			if ((has_image != undefined) && (has_image != ""))
				doc_image = "http://howtosmile.org/screenshots/photo.php?colID="+col_id+'&recID='+doc_id;
			else
				doc_image = "http://www.howtosmile.org/screenshots/noimage_thumb.jpg";
			return doc_image;
		}
		
		/** Add prev and next page links. */
		function _showPaginationLinks()
		{
			var html = '';
			if( results != null )
			{
				var numfound = parseInt(results.numresults);
				var perpage = parseInt(results.numperpage);
				var thisPage = _getCurrentPage();
				var totPages = _getPagesTotal();
				var html = '';
				if( thisPage > 1 )
					html += ' <a href="#" onclick="'+widgetName+'.prevPage();return false;"><<</a>&nbsp;&nbsp;';
				html += 'page '+thisPage+'/'+totPages;
				if( totPages > thisPage )
					html += ' &nbsp;&nbsp;<a href="#" onclick="'+widgetName+'.nextPage();return false;">>></a>';
			}
			document.getElementById(widgetName+'_pagination' ).innerHTML = html;
		}
		
		/** Return page number currently showing. */
		function _getCurrentPage()
		{
			if( results == null )
				return null;
			return Math.floor( parseInt(results.offset) / parseInt(results.numperpage) ) + 1;
		}
		
		/** Return number of pages of results for most recently executed query. */
		function _getPagesTotal()
		{
			if( results==null )
				return 0;
			return Math.ceil( parseInt(results.numresults) / parseInt(results.numperpage) );
		}
		
		/**
		 * show individual record display
		 * @param num index of desired record in array of results for current page
		 */
		function _showRecord(num) {
			var html = _formatLongRecord(current_results[num]);
			//alert(output);
			document.getElementById(widgetName+'_record_inner').innerHTML = html;
			
			_fixRecordHeight();
			
			var elem =document.getElementById(widgetName+'_result_record');
			if( window.Animator != undefined )
			{
				if( animator==null )
					animator  = new Animator({duration:200,interval:20}).addSubject( new CSSStyleSubject( elem, "showing" ) );
				animator.play();
			}
			else
			{
				_setClass(elem, "showing");
			}
			
			recordShowing = true;
		}
		
		/** hide currently showing full record (if any is showing). */
		function _hideRecord() {
			if( !recordShowing )
				return;
			if( animator!=null )
				animator.reverse();
			else
			{
				_removeClass(document.getElementById(widgetName+'_result_record'));
			}
			recordShowing = false;
		}
		
		/**
		 *Return shortened version of a text string (truncated on space character).
		 * @param str, String
		 * @param len, Int, Maximum length of returned string.
		 */
		function _shortenText(str,len) {
			newstr = str;
			if (str.length > len)   {
				newstr = str.substring(0,len);
				lastspace = newstr.lastIndexOf(' ');
				newstr = newstr.substring(0,lastspace);
			}
			return newstr;
		}
		
		
		
		function _startScroll(direction)
		{
			// clear previous interval just in case
			clearInterval(scrollIntervalId);
			// move one notch then set interval to keep scrolling while button is held down
			_scrollList( direction, scrollIncrement );
			scrollIntervalId = setInterval( function(){ _scrollList(direction, scrollIncrement ); }, scrollInterval );
		}
		
		/** Stop any current scrolling of the _list div. */
		function _stopScroll()
		{
			clearInterval( scrollIntervalId );
		}
		
		/**
		 * Move the _list div.
		 * @param direction, String, either 'backward' (to move the div down) or 'forward' (to move the div up).
		 * @param increment, Int, how far to move the div (pixels).
		 */
		function _scrollList(direction, increment)
		{
			var elem =document.getElementById(widgetName+'_list');
			var containerHeight = document.getElementById( widgetName+'_list_holder').offsetHeight;
			var listHeight = elem.offsetHeight;
			if( listHeight <= containerHeight ) // scrolling not needed
				return false;
			
			var retVal = true;
			
			var incr = (direction == 'forward') ? -increment : increment;
			
			var currVal = parseInt(elem.style.top);
			if( isNaN(currVal) )
				currVal = 0;
			var newVal = currVal + incr;
			
			if( newVal > 0 ) // keep it from going down too far
			{
				newVal = 0;
				retVal = false;
			}
			if( newVal < containerHeight - listHeight ) // keep from going up too far
			{
				newVal = containerHeight - listHeight;
				retVal = false;
			}
			
			elem.style.top = newVal + "px";
			return retVal;
		}
		
		/**
		 * Enable scrolling button event handlers.
		 * @param direction, String, either "backward" or "forward"
		 */
		function _enableScroll( direction )
		{
			document.getElementById(widgetName+'_list'+direction).onmousedown = function(){ _startScroll(direction); };
			document.getElementById(widgetName+'_list'+direction).onmouseup = function(){ _stopScroll(); };
		}
		
		/** Enable scrolling keyboard event handlers. */
		function _enableKeyScrollEvents()
		{
			var keydownlistener = function(evt){
				evt = evt || document.event;
				var cancel = false;
				if( evt.keyCode == 40 )
					cancel = _scrollList('forward',scrollIncrement);
				else if( evt.keyCode == 38 )
					cancel = _scrollList('backward',scrollIncrement);
				if( cancel )
					_cancelEvent(evt);
			}
			
			var keyuplistener = function(evt){
				_stopScroll();
				evt = evt || document.event;
			}
			
			document.getElementById(widgetName+'_list').onmouseover = function(){
				_addEvent(document, 'keydown', keydownlistener);
				_addEvent( document, 'keyup', keyuplistener );
			};
			document.getElementById(widgetName+'_list').onmouseout = function(){
				_removeEvent( document, 'keydown', keydownlistener );
				_removeEvent( document, 'keyup', keyuplistener );
			};
		}
		
		/** empty the page data cache */
		function _clearCachedPages()
		{
			cachedPageData = {};
		}
		
		/* ====== helper functions for cross-browser stuff ========== */
		function _setClass(elem, className)
		{/* IE does this differently than other browsers */
			elem.setAttribute( "class", className ); // for non IE
			elem.setAttribute( "className", className );//for IE
		}
		function _removeClass(elem)
		{
			elem.removeAttribute("class"); // for non IE
			elem.removeAttribute("className"); // for IE
		}
		
		function _addEvent( elem, event, listener )
		{
			if( elem.addEventListener )
				elem.addEventListener(event, listener, false );
			else if( elem.attachEvent )// IE
				elem.attachEvent( 'on'+event, listener );
		}
		function _removeEvent( elem, event, listener )
		{
			if( elem.removeEventListener )
				elem.removeEventListener( event, listener, false );
			else if( elem.detachEvent )// IE
				elem.detachEvent( 'on'+event, listener );
		}
		function _cancelEvent( evt )
		{
			if (evt.stopPropagation)
				evt.stopPropagation();
			else
				evt.cancelBubble=true; // IE
			
			if( evt.preventDefault )
				evt.preventDefault();
			else
				evt.returnValue = false; // IE
		}
		
		function _setParams( oParams )
		{
			// clear cached pages if search is going to change
			_clearCachedPages();
			for( p in oParams )
			{
				_setSearchParam( p, oParams[p] );
			}
		}
		
		function _initWidget( containerElem, initialParams, oConfig )
		{
			//this.setParams( initialParams );
			_setParams(initialParams);
			_setConfig( oConfig );
			var html = _createWidgetHtml();
			if( containerElem==null )
				document.write( html );
			else
				containerElem.innerHTML = html;
			
			if( config.startHidden )
				_hideContainer();
			
			if( ! config.deferSearch )
				_submitSearch();
			else
				_hideSpinner();
			if( config.showPersistentSearch )
				_initSearchBar();
			if( config.headerUrl )
				_initHeaderImage();
			_fixListholderHeight();
		}
		
		function _hideContainer()
		{
			_getContainer().style.display = 'none';
		}
		function _showContainer()
		{
			_getContainer().style.display = 'block';
		}
		function _getContainer()
		{
			return document.getElementById(widgetName+'_container');
		}
		
		/*
		 * return the publically available members of the widget
		 */
		return {
			/**
			 * output the widget to the host HTML page
			 * @function
			 * @param containerElem
			 *   DOM element whose content will be replaced by the widget. If null,
			 *   document.write() will be used to insert the widget at the script's location
			 * @param initialParams
			 *   object initialization values for the widget behavior/search terms
			 * @param oConfig
			 *   object configuration options for the smile widget see {@link smileWidget-config}
			 *   for a full list of configurable options
			 * @since Object oConfig replaces Boolean deferSearch as 3rd the parameter since 1.1
			 * @example
			 * smileWidget.initWidget( 
			 *	document.getElementById('widgetIsInHere'),
			 *	{
			 *		'keywords':["graphing"]
			 *	},
			 *	{
			 *		noResultsHTML:smileWidget.HTML_SEARCH_SMILE_FORM,
			 *		queryTimeout:timeout,
			 *		queryTimeoutCallback:function(){
			 *			alert('no results received by timeout of '+timeout+' msec');
			 *		}
			 *	}
			 * );
			 */
			initWidget: _initWidget,
			
			/**
			 * set widget search parameters as key->value pairs. Previously 
			 * set parameters will NOT be unset if their key is not
			 * provided. To unset a parameter, submit it with a null value.
			 * @function
			 * @example
			 *    smileWidget.setParams(
			 *		{
			 *			institution:['The Exploratorium'],
			 *			keywords:null
			 *		}
			 *    );
			 * @param oParams
			 *    object, key->value pairs of parameters defining widget 
			 *    behavior and search terms. See documentation for full
			 *    list of valid members.
			 */
			setParams: _setParams,
			
			/**
			 * DEPRECATED: Configure widget when calling initWidget() instead.
			 * Set widget configuration. Config options provided as key->value 
			 * pairs. (Call setConfig BEFORE {@link smileWidget.initWidget} unless you're passing 'true'
			 * as the value to initWidget's deferSearch parameter.)
			 * @function
			 * @deprecated Configure widget when calling initWidget() instead
			 * @see smileWidget-config for a list of configurable options
			 * @param oConfig
			 *    object, key->value pairs of configurable options for the widget.
			 *
			 * @since 1.1
			 */
			setConfig: _setConfig,
			
			/**
			 * submit search based on currently set values and display results
			 */
			search: function()
			{
				_clearCachedPages();
				_submitSearch();
			},
			
			/**
			 * Not intended to be called by the user, this is a callback 
			 * meant to be executed by a newly loaded data script after a 
			 * search or page change has retrieved data from the server.
			 * @function
			 * @param res
			 *    data returned from the server
			 */
			dataLoaded: _receivedResults,
			
			/**
			 * Show full data view for a record
			 * @function
			 * @param num
			 *    index number of desired record in the currently loaded data set
			 */
			showRecord: _showRecord,
			
			/**
			 * hide full data record if it is currently showing
			 * @function
			 */
			hideRecord: _hideRecord,
			
			/**
			 * load (if not already cached) and display next page of data for current search
			 */
			nextPage: function()
			{
				_submitSearch( _getCurrentPage()+1 );
			},
			
			/**
			 * load (if not already cached) and display previous page of records for current search
			 */
			prevPage: function()
			{
				_submitSearch( _getCurrentPage()-1 );
			},
			
			/**
			 * Return the version string of this widget
			 */
			getVersion: function()
			{
				return widgetVersion;
			},
			/**
			 * html markup (suitable for use as a configuration parameter to {@link smileWidget.setConfig}) of a form
			 * to search the SMILE database (loads SMILE website in a new window)
			 * @since 1.1
			 */
			HTML_SEARCH_SMILE_FORM: 'Search the SMILE database for resources:'
			 			   + '<form action="http://howtosmile.org/resourcesearch" target="_blank">'
						   + '<input type="text" name="ddssearch_q"/>'
						   + '<input type="hidden" name="verb" value="Search"/>'
						   + '<input type="submit" value="go">'
						   + '</form>'
		};
	}();
}

