/*
* Proof of concept, instant search widget against RichRelevance APIs.
*
* 2014 RichRelevance, Inc.
* @bargar
*
* Modified version of jQuery Search as you Type plugin - http://drawne.com/demo/sayt.js/
*
* version 14.05.10
*/
(function ($) {
$.fn.sayt = function (options) {
var boxObj;
var getInputWidth = $(this).outerWidth() - 2;
// Keeps track of the width of the input box in case user resizes browser
var resizeTimer;
$(window).resize(function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
var getInputWidth = $(this).outerWidth() - 2;
boxObj.hide();
}, 200);
});
// Default options
var defaults = {
// id and class to apply to the dropdown list of matches
inputId: '%-sayt',
classPrefix: 'sayt-',
// width of dropdown list of matches, this will be based on the
// text input unless overridden with a numerical value
inputWidth: getInputWidth,
inputPosition: "absolute",
inputTop: "39px",
inputRight: "0px",
// characters user must enter before search apis are called
minChars: 1,
// when true, when user selects a search term, populate the text input with the selected term
setInputValueOnSelection: false,
// fade in/out animation for dropdown list
fadeTime: 200,
// jquery ajax timeout, 0 means don't apply timeout
ajaxTimeout: 0,
// true to turn on widget debug logging
debug: false,
// set to numerical value to render product images as specific dimensions regardless of source size
imageHeight: false,
imageWidth: false,
// set to true to replace leading http:// or https:// in image urls with schemeless // prefix to work
// in http and https contexts without browser warnings
removeImageProtocol: false,
// set to a string value to append that string set to true to url's clicked in search results
// also set as true as var in containing search form for submit of search term search
sourceIsInstantSearchBooleanQueryParam: false,
// name of parameter used in products api call to indicate name of callback function
productsJsonpCallbackParameterName: 'callback',
// client code limitation on products, note api may also have input on product count
maxProducts: 5,
// time in milliseconds between the hover of search term and new recs being called
pauseBetweenMouseover: 400,
// set to true to show the strategy message in the products recommendation placement
showStrategyMessage: true,
// time in milliseconds between the hover of search term and new recs being called
noResultsMessage: ""
};
var options = $.extend(defaults, options);
var logger = RRsayt.getLogger({
enabled: options.debug,
prefix: 'sayt: '
});
logger.log('debug log active');
if (!options.termsUrl) {
logger.log('termsUrl is a required option');
return;
}
if (!options.productsUrl) {
logger.log('productsUrl is a required option');
return;
}
options.inputId = options.inputId.replace('%', $(this).attr('id'));
var prevQuery = '';
var data;
$(this).attr('autocomplete', 'off');
$(this).after('
');
var input = $(this);
boxObj = $('#' + options.inputId);
//Actual function with keyup
var currentTermRequest = null;
var currentLeadTerm = null;
var currentTermsHtml = '';
var currentProductsHtml = '';
var requestCount = 0;
var productRequestCount = 0;
var inputListNavigationHandler = null;
input.keyup(function (event) {
//only trigger on keystokes
var keyCode = event.which;
if (keyCode != 13 && keyCode != 9 && keyCode != 37 && keyCode != 38 && keyCode != 39 && keyCode != 40) {
//remove leading spaces and apostrophes, set to lowercase
var query = input.val().toLowerCase().replace(/^[ \t]+|\'/g, "");
if (query == "" || query.length < options.minChars) {
//do nothing if min value isn't met
boxObj.fadeOut(options.fadeTime);
} else {
if (prevQuery === query) {
// user input is unchanged, don't call out to api
return;
}
prevQuery = query;
// could be used for loading animation
input.addClass(options.classPrefix + 'thinking');
currentTermRequest = $.ajax({
url: options.termsUrl + query,
crossDomain: true,
contentType: "application/json",
timeout: options.ajaxTimeout,
scriptCharset: 'UTF-8',
dataType: "jsonp",
requestCount: ++requestCount,
success: function (data) {
logger.log('requestCount: ' + requestCount + ', this.requestCount: ' + this.requestCount);
if (requestCount !== this.requestCount) return;
logger.log(' run');
var output = "";
var resultsExist = data.length > 0;
if (!resultsExist) {
// hide suggestions box completely when no results
boxObj.hide();
// set display text for 'no-keyword' results
// output = "" + options.noResultsMessage + query + "
";
// make requst for products from query
var term = query;
var type = "search";
var id = "";
requestRelatedProducts(term,type,id);
} else {
// show suggestions box when results
boxObj.show();
// sort by score
data.sort(function(a, b) {
return b.score - a.score;
});
suggestedSearchTerms = jQuery.map(data, function(datum) {
return {
title: datum.term,
type: datum.type,
id: datum.id
}
});
var firstTerm = suggestedSearchTerms[0].title;
var firstType = suggestedSearchTerms[0].type;
var firstId = suggestedSearchTerms[0].id;
// suggested term has changed
if (currentLeadTerm !== firstTerm) {
// clear current suggested products html
currentProductsHtml = '';
currentLeadTerm = firstTerm;
currentLeadType = firstType;
currentLeadId = firstId;
if (firstTerm !== null) {
// kick off a new request for products related to the current lead term
var term = firstTerm;
var type = firstType;
var id = firstId;
requestRelatedProducts(term,type,id);
}
}
// section iteration
currentTermsHtml = generateLiHtml(suggestedSearchTerms, 'term', query);
output += '';
output += currentTermsHtml;
output += '
';
output += '';
output += currentProductsHtml;
output += '
';
}
// render list options
boxObj.html(output);
// boxObj.css('width', options.inputWidth);
boxObj.css('position', options.inputPosition);
//boxObj.css('top', options.inputTop);
//var widthDifference = options.inputWidth - $(input).width() - 22;
//boxObj.css('left', $(input).position().left - widthDifference);
//boxObj.css('left', 0);
//remove thinking class
input.removeClass(options.classPrefix + 'thinking');
// set up keyboard interaction with options
inputListNavigationHandler = setUpKeyboardHandling(inputListNavigationHandler);
// commenting out, as boxes are hidden by default
//boxObj.fadeIn(options.fadeTime);
return;
},
error: function (xhr, textStatus, errorThrown) {
logger.log('error on jsonp response: ' + textStatus + ' ' + errorThrown);
}
});
}
}
});
//show last results if input focused again
input.focus(function () {
if (input.val() != '' && boxObj.html().length != 0) {
boxObj.fadeIn(options.fadeTime);
}
});
//hide search when input if unselected
input.blur(function () {
boxObj.fadeOut(0);
});
var generateLiHtml = function(items, clazz, query) {
if (clazz) {
} else {
clazz = '';
}
var output = '';
var i = 0;
// item iteration
$.each(items, function (ii, item) {
var haslink = item.url != undefined;
var link = '';
if (haslink) {
link = "";
} else {
var saytElement = '$(\'#' + options.inputId + '\')';
// set up div
link = '';
}
var linkclose = haslink ? "" : "
";
output += '' + link;
output += ''
if (item.image != undefined) {
var imageSrc = item.image;
if (options.removeImageProtocol) {
imageSrc = RRsayt.removeProtocolFromUrl(imageSrc);
}
var imageDimensions = '';
if (options.imageHeight) {
imageDimensions += ' height="' + options.imageHeight + '" ';
}
if (options.imageWidth) {
imageDimensions += ' width="' + options.imageWidth + '" ';
}
// CUSTOM IMAGE SIZING
output += ' | ';
}
output += '';
output += ' ';
// CUSTOM DISPLAY
//if product has brand list it first
if (item['brand'] != undefined) {
var brand = item.brand;
if (clazz === 'term' && query) {
brand = RRsayt.wrapSubstringInSpan(brand, query, 'userInput');
}
output += '' + brand + ' \n';
}
if (item['title'] != undefined) {
var title = item.title;
if (clazz === 'term' && query) {
title = RRsayt.wrapSubstringInSpan(title, query, 'userInput');
}
output += '' + title + ' \n';
}
if(item.notonsale){
output += (item.originalprice != undefined) ? '' + item.originalprice+'' : '';
}
output += (item.description != undefined) ? '' + item.description + '' : '';
output += ' ';
output += ' | ';
output += '
';
output += linkclose;
output += '';
i++;
});
return output;
};
var setUpKeyboardHandling = function(previousHandler) {
// unbind previous bindings
if (previousHandler) {
input.unbind('keyup', previousHandler);
}
// use keyboard for navigating through and selecting TERM results only (not products)
var current_index = -1,
$number_list = $('.' + options.classPrefix + 'box'),
$options = $number_list.find('.' + options.classPrefix + 'result.term'),
items_total = $options.length;
$options.hover(function () {
$options.removeClass('selected');
$(this).addClass('hover selected');
current_index = $options.index(this);
var $selection = $options.eq(current_index);
var selectedTerm = $.trim($selection.text());
var dataType = $(this).attr("data-type");
var dataId = $(this).attr("data-id");
var dataTerm = $(this).attr("data-term");
window.termHover = setTimeout(function(){
// display query in input box when hovering
var selectedTerm = $.trim($selection.text());
input.val(selectedTerm);
if (options.sourceIsInstantSearchBooleanQueryParam) {
$(':input[name=' + options.sourceIsInstantSearchBooleanQueryParam + ']', $(input).closest('form')).val('true');
}
if (selectedTerm && currentLeadTerm !== selectedTerm) {
// kick off a new request for products related to the current lead term
term = selectedTerm;
type = dataType;
id = dataId;
requestRelatedProducts(term,type,id);
}
currentLeadTerm = selectedTerm;
}, options.pauseBetweenMouseover);
}, function () {
$(this).removeClass('hover selected');
current_index = -1;
clearTimeout(window.termHover);
});
var newHandler = function (e) {
if (e.which == 40) {
if (current_index + 1 < items_total) {
current_index++;
change_selection();
}
input.val(input.val());
e.preventDefault();
} else if (e.which == 38) {
if (current_index > 0) {
current_index--;
change_selection();
}
input.val(input.val());
e.preventDefault();
} else if (e.which == 13) {
if (!$options.eq(current_index).hasClass('hover') && current_index > -1) {
var newLocation = $options.eq(current_index).find('a').attr("href");
if (newLocation) {
window.location = newLocation;
e.preventDefault();
}
}
;
}
};
input.bind('keyup', newHandler);
function change_selection() {
$options.removeClass('selected');
$options.removeClass('hover');
var $selection = $options.eq(current_index);
$selection.addClass('selected');
if (options.setInputValueOnSelection) {
if (!$('a', $selection).length) {
// no anchor, this is a simple term, not a product with a link
var selectedTerm = $.trim($selection.text());
input.val(selectedTerm);
if (options.sourceIsInstantSearchBooleanQueryParam) {
$(':input[name=' + options.sourceIsInstantSearchBooleanQueryParam + ']', $(input).closest('form')).val('true');
}
if (selectedTerm && currentLeadTerm !== selectedTerm) {
// kick off a new request for products related to the current lead term
var seed = selectedTerm;
var type = "";
var id = "";
requestRelatedProducts(term,type,id);
}
currentLeadTerm = selectedTerm;
}
}
}
return newHandler;
};
var requestRelatedProducts = function(term,type,id) {
var reqString = "&searchTerm=" + term;
if (type === "category"){
reqString += "&chi=" + id;
}else if (type === "brand"){
reqString += "&fpb=" + term;
}else if (type === "product"){
reqString += "&fpb=" + term;
}
// kick off a new request for products related to the current lead term
$.ajax({
url: options.productsUrl + reqString,
crossDomain: true,
contentType: "application/json",
dataType: "jsonp",
jsonp: options.productsJsonpCallbackParameterName,
timeout: options.ajaxTimeout,
productRequestCount: ++productRequestCount,
success: function (response) {
if (productRequestCount !== this.productRequestCount) return;
// remove products in dropdown
$('.product', boxObj).remove();
var output = "";
// support multiple apis, check for recs anywhere and standard recs responses
var resultsExist = false;
var products = null;
if (response.placements
&& response.placements.length
&& response.placements[0].recommendedProducts
&& response.placements[0].recommendedProducts.length > 0) {
logger.log('products api: standard recs');
resultsExist = true;
products = response.placements[0].recommendedProducts;
} else if (response.recommendedProducts && response.recommendedProducts.length > 0) {
logger.log('products api: recs anywhere');
resultsExist = true;
products = response.recommendedProducts;
}
// ensure that we have no more than options.maxProducts, regardless of what api provides
if (products && products.splice) {
products = products.splice(0, options.maxProducts);
}
if (resultsExist) {
// transform products api response to form expected by sayt
var saytData = [];
$.each(products, function (ii, rec) {
var price = '',pricefixed='';
var originalprice = '',originalfixedprice='';
var notonsale = false;
if (rec.priceCents && rec.priceCents.toFixed) {
pricefixed = (rec.priceCents / 100).toFixed(0);
price = '$' + pricefixed;
}
try {
if (rec.attributes.original_price && !isEmpty(rec.attributes.original_price)) {
originalfixedprice =parseFloat(rec.attributes.original_price).toFixed(0);
originalprice = '$' + originalfixedprice;
if (!(parseFloat(pricefixed) === parseFloat(originalfixedprice))){
notonsale = true;
}
}
}
catch (err) {
notonsale = false;
}
var name = rec.name;
var brand = '';
if (rec.brand) {
brand = rec.brand;
}
saytData.push({
brand: brand,
title: name,
description: price,
url: rec.clickURL,
image: rec.imageURL,
originalprice: originalprice,
notonsale: notonsale
});
});
// product section iteration
currentProductsHtml = generateLiHtml(saytData, 'product');
output += '';
output += currentProductsHtml;
output += '
';
boxObj.append(output).show();
}
}});
function isEmpty(val){
return (val === undefined || val == null || val.length <= 0) ? true : false;
}
};
return this;
}
})(jQuery);
RRsayt = {
// for styling search term results so that the portion of the string in the result matching the user's input is
// emphasized
wrapSubstringInSpan: function(string, substring, wrapClass) {
var result = string;
if (string && substring && wrapClass) {
var parts = string.split(substring);
if (parts.length > 1) {
if (parts[0] === '') {
parts.shift();
result = '' + substring + '' + parts.join(substring);
}
}
}
return result;
},
getLogger: function(options) {
return {
log: function (message) {
if (options.enabled) {
if (options.prefix) {
message = options.prefix + ' ' + message;
}
console.log(message);
}
}
};
},
removeProtocolFromUrl: function(url) {
if (url && url.toLowerCase) {
var httpPrefix = 'http://';
var httpsPrefix = 'https://';
if (url.toLowerCase().indexOf(httpPrefix) === 0) {
url = url.substring(httpPrefix.length - 2);
} else if (url.toLowerCase().indexOf('https://') === 0) {
url = url.substring(httpsPrefix.length - 2);
}
}
return url;
}
};