/* $Id: autosuggest.js,v 1.15 2008/07/31 14:50:19 nickc Exp $
(c) 2008 The New York Times Company
*/
// common object for member center ticker alerts
var TickerAlertCommon = { safe: 'result.ticker', preventSubmit: true, selectFirst: true, disableFunds: true, paramName: 'query' }
/**
* Configuration variables specific to Business Autosuggest
* @constructor
*/
Business = {
/**
* @param {Array} Inputs An array of inputs used in autosuggest,
* an item can either be a string representing the ID or an object
* containing setup information:
* { input: id,
* placeholder: text (optional)
* placeholder_class: class (optional)
* safe: evaled js (optional) Place in input field
* preventSubmit: boolean (optional) Prevents redirecting to URL on selection
* selectFirst: boolean (optional) Selects first item on searching
* forceLeft: boolean (optional) Forces the autosuggest results box to the left
* // Extra: any option defined within Ajax.Autocompleter can go here
* // and will be passed on creation
* }
*/
Inputs: [ // regular page inputs
{ input: 'bsearchQuery', // subnav input
placeholder: 'News, Stocks, Funds, Companies',
placeholder_class: 'greyed' },
{ input: 'qsearchQuery', // module input
placeholder: 'Stocks, ETFs, Funds',
placeholder_class: 'greyed',
paramName: 'query' },
// create ticker alert inputs (member center)
Object.extend(Object.clone(TickerAlertCommon), { input: 'tickername_1' }),
Object.extend(Object.clone(TickerAlertCommon), { input: 'tickername_2' }),
Object.extend(Object.clone(TickerAlertCommon), { input: 'tickername_3' }),
Object.extend(Object.clone(TickerAlertCommon), { input: 'tickername_4' }),
Object.extend(Object.clone(TickerAlertCommon), { input: 'tickername_5' })
],
/**
* Defines autosuggest URI based on the hostname
*
* @param {String} h Supply a hostname instead of
* basing it off the window hostname
* @returns {String} Autosuggest Service URI
*/
SuggestServer: function(h) {
switch (h || window.location.hostname) {
// blogs, currently disabled
case 'shiftingcareers.blogs.nytimes.com':
case 'norris.blogs.nytimes.com':
case 'dealbook.blogs.nytimes.com':
return false;
break;
// wsod
case 'smarkets.on.nytimes.com':
case 'markets.on.nytimes.com':
case 'nytimes.wsodqa.com':
return '/services/autocomplete/autocomplete.asp';
break;
// debug
case 'localhost':
return '_return.php';
break;
// default
default:
return '/svc/search/business/autosuggest';
}
},
/**
* Returns the funds redirect url
*
* @param {String} s Ticker to be appended to url
* @returns {String} URL for Funds redirect
*/
FundURL: function(s) {
return 'http://markets.on.nytimes.com/research/markets/usmarkets/snapshot.asp?symbol=' + s;
},
/**
* @param {Template} Template Defines template used for pushing into UL
*/
Template: new Template('
#{ticker}#{company}')
}
/**
* Business.Autosuggest is an extension of Ajax.Autocompleter
* defined by scriptaculous. This extension adds specific
* support for NYT service-based autocomplete.
*
* Assumes results divider is below (next) to the search
* input.
*
* Requires Scriptaculous 1.8.1 or above (1.8.0 has a bug)
*
* @constructor
* @base Ajax.Autocompleter
* @requires Business.Config Required for configuration variables
*/
Business.Autosuggest = Class.create();
Object.extend(Object.extend(Business.Autosuggest.prototype, Ajax.Autocompleter.prototype), {
initialize: function(element, update, url, options) {
options = options || {};
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.options.method = 'get';
this.options.autoSelect = false; // prevent autoselecting if only one result
this.options.minChars = 2;
this.options.frequency = 0;
this.options.selectFirst = options.selectFirst || false;
this.options.preventSubmit = options.preventSubmit || false;
this.options.disableFunds = options.disableFunds || false;
this.url = url;
this.urls = [];
},
/**
* Called on a successfull complete of the service call
*/
onComplete: function(request) {
this.processResponse(request.responseText.evalJSON());
},
/**
* Overwriting to remove scrolling into view as this breaks
* functionality. Why was this added?
* @private
*/
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
/**
* Overwriting to remove scrolling into view as this breaks
* functionality. Why was this added?
* @private
*/
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
/**
* Fires when a user selects an entry, this will redirect the
* user to the page supplied within the URLs array
*/
selectEntry: function() {
this.active = false;
var entry = this.getCurrentEntry();
if(entry) {
this.updateElement(entry);
this.element.parentNode.onsubmit = function() { return false; } // firefox 2 mac bug
if( !this.options.preventSubmit ) { window.location = this.urls[entry.autocompleteIndex]; }
} else {
this.element.parentNode.submit();
}
},
/**
* Updates the search input with the selected value
*
* @param {Element} selectedElement DOM element selected by user
*/
updateElement: function(selectedElement) {
this.element.setValue(selectedElement.readAttribute('title'));
this.element.focus();
},
/**
* Processes the JSON response into a properly formatted list
*
* @param {Object} json The json object initially returned
* by the ajax call
* @requires Business.Config Defines HTML template
*/
processResponse: function(json) {
var html = [];
var keyword = json[0];
var results = json[1];
this.urls = [];
this.entryCount = 0;
html.push('')
/**
* Loop through the results array and push to UL
*/
for(var i = 0; i < results.length; i++ )
{
var result = eval('(' + results[i] + ')').results;
var re = new RegExp('(' + keyword + ')', 'i');
// validate and skip on false
if ( !this.validateResult(result) ) continue;
this.urls.push((this.isFund(result)) ? Business.FundURL(result.ticker) : result.url);
html.push(this.options.template.evaluate({
ticker: result.ticker.replace(re, '$1'),
company: result.company.replace(re, '$1'),
output_safe: ( this.options.safe ) ?
eval( this.options.safe ) : result.company
}));
this.entryCount++;
}
html.push('
');
this.updateChoices(html.join(''));
if( !this.options.selectFirst ) this.makeInactive();
},
/**
* Not so pretty way of not selecting the first result by
* default; this removes the selected class and sets the
* index to a negative value negating the selectEntry func
*/
makeInactive: function() {
this.update.getElementsByTagName('li')[0].removeClassName('selected');
this.index = -1;
},
/**
* Returns true or false dependent on weather the result
* is a fund, i.e. contains company and ticker but no url
*
* @returns True or false
*/
isFund: function(result) {
return ( result.company && result.ticker && !result.url );
},
/**
* Returns true or false based on whether the object passes
* validation or not.
*
* @param {Object} result Result object passed to function
* @returns True or false
* @type Boolean
*/
validateResult: function(result) {
// if entirely empty return false
if ( !result.company || !result.ticker || ( !this.isFund(result) && !result.url) )
return false;
// if the result is a fund, check that we want to display it
if ( this.isFund(result) && this.options.disableFunds )
return false;
// defaults to true
return true;
}
});
/**
* Creates a placeholder, or default input text, for the
* supplied element.
*
* @param {Element} el Search input
* @param {String} text Placeholder Text
* @param {String} class Placeholder class name
*/
var createPlaceholder = function(el, text, css_class) {
var searchInput = $(el);
// if value is present, quietly exit
if ( searchInput.present() ) return;
// set default text and class
if ( css_class ) $(el).addClassName( css_class );
$(el).setValue( text );
/**
* Observe focus, if input value equals placeholder value
* then clear the input and remove the class
*/
searchInput.observe('focus', function(){
if ( this.value == text ) {
this.clear();
if ( css_class ) this.removeClassName( css_class );
}
});
/**
* Observe blur, if input value is empty, replace with default
* placeholder value and add class back
*/
searchInput.observe('blur', function(){
if ( !this.present() ) {
if ( css_class ) this.addClassName( css_class );
this.setValue( text );
}
});
// prevent placeholder text from being submitted by accident
searchInput.parentNode.onsubmit = function(){
if ( $(el).value == text ) $(el).clear();
return true;
}
}
/**
* Fires on document ready, initializes autosuggest object
*/
document.observe("dom:loaded", function(){
var page = Business.SuggestServer();
var template = Business.Template;
// get the inputs, if there is only one, push it into an array
var inputs = ( typeof(Business.Inputs) != 'object' ) ?
[Business.Inputs] : Business.Inputs;
// only create object if on correct host and has defined inputs
if ( page != false && inputs )
{
// loop through supplied inputs and create autosuggest object
inputs.each(function(i) {
// using placeholder? if so, get the input from object
var input = ( typeof(i) == 'object' ) ? $(i.input) : $(i);
var update = (input) ? input.next("div.querySuggestions") : update = null;
// check for existence & create
if( input && update )
{
// force left if set
if ( i.forceLeft ) update.addClassName('forceLeft');
// deal with options - remove default (not used by autosuggest)
var options = ( typeof i == 'object' ) ? Object.clone(i) : {};
options.input = options.placeholder = options.placeholder_class = options.forceLeft = null;
options.template = template;
// finally create the object
new Business.Autosuggest(input, update, page, options);
}
// create placeholder if defined
if ( input && typeof i == 'object' && i.placeholder )
createPlaceholder(input, i.placeholder, i.placeholder_class)
});
}
});