// Use the pattern below, just in case it is already created some place else
window.Pcln = (window.Pcln) ? window.Pcln : {};
//Built on top of JQuery datepicker: http://api.jqueryui.com/datepicker/#option-beforeShow
//
Pcln.datePicker = function(dateJQElem, options, pricelineUrl) {
this.options = {
numberOfMonths: 1,
showOn: "both",
buttonImage: pricelineUrl + "/landing/content/graphics/lib/form/calendar-icon.png",
buttonImageOnly: true,
buttonText: "Calendar Icon",
minDate: '+0d',
maxDate: '+330d',
onSelect : this.onSelect.bind(this),
onClose : this.onClose.bind(this)
};
// allow the caller to overload any of the initialization properties.
if (options) {
$.extend(this.options, options);
}
// Set up date picker
this.dateJQElem = dateJQElem;
this.picker = dateJQElem.datepicker({
numberOfMonths: this.options.numberOfMonths,
showOn: this.options.showOn,
buttonImage: this.options.buttonImage,
buttonImageOnly: this.options.buttonImageOnly,
buttonText: this.options.buttonText,
minDate: this.options.minDate,
maxDate: this.options.maxDate,
onSelect : this.options.onSelect,
onClose : this.options.onClose
});
return this;
};
Pcln.datePicker.prototype = {
getSelectedDate: function() {
return this.dateJQElem.datepicker("getDate");
},
setDate: function(newDate) {
this.dateJQElem.datepicker("setDate", newDate);
},
setMinDate: function(minDate) {
this.dateJQElem.datepicker("option", "minDate", minDate);
},
setMaxDate: function(maxDate) {
this.dateJQElem.datepicker("option", "maxDate", maxDate);
},
setMinMaxDate: function(minDate, maxDate) {
this.setMinDate(minDate);
this.setMaxDate(maxDate);
},
// methods that can be overwritten by the caller
onSelect: function(selectedDate, inst) {
// get the number of days from the current day
var one_day=1000*60*60*24;
var daysDiff=Math.ceil((this.getSelectedDate().getTime() - new Date().getTime()) / (one_day));
},
onClose: function(dateText, inst) { }
};
// Class for creating a date in and out pairing.
Pcln.datePair = function(dateInJQElem, dateOutJQElem, options, dateInOptions, dateOutOptions, pricelineUrl) {
this.options = {
focusAfterDateIn : dateOutJQElem,
focusAfterDateOut : null,
rangeInDays : 330
};
this.dateInOptions = {
onClose : this.onCloseDateIn.bind(this),
onSelect: this.onSelectDateIn.bind(this)
};
this.dateOutOptions = {
onClose : this.onCloseDateOut.bind(this)
};
// allow the caller to overload any of the initialization properties.
if (options) {
$.extend(this.options, options);
}
if (dateInOptions) {
$.extend(this.dateInOptions, dateInOptions);
}
if (dateOutOptions) {
$.extend(this.dateOutOptions, dateOutOptions);
}
// Set up date pickers
this.dateIn = new Pcln.datePicker(dateInJQElem, this.dateInOptions, pricelineUrl);
this.dateOut = new Pcln.datePicker(dateOutJQElem, this.dateOutOptions, pricelineUrl);
};
Pcln.datePair.prototype = {
onSelectDateIn: function(selectedDate, inst) {
var origDate = $.datepicker.parseDate(inst.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, inst.settings);
// set the range for the ending date.
// min
var date = this.addDays(origDate, 1);
this.dateOut.setMinDate(date);
// max
var selectedDateObj=new Date(origDate);
this.dateOut.setMaxDate(this.addDays(selectedDateObj, this.options.rangeInDays));
// set the end date.
var dayPlusOne = this.addDays(selectedDateObj, 1);
this.dateOut.setDate(dayPlusOne);
// call the default method:
this.dateIn.onSelect(selectedDate, inst);
},
addDays: function(selectedDateObj, noDays) {
var newDate=new Date(selectedDateObj);
var days=newDate.getDate() + noDays;
newDate.setDate(days);
return newDate;
},
onCloseDateIn: function(dateText, inst) {
if (this.options.focusAfterDateIn) {
this.options.focusAfterDateIn.focus();
}
this.dateIn.onClose(dateText, inst);
},
onCloseDateOut: function(dateText, inst) {
if (this.options.focusAfterDateOut) {
this.options.focusAfterDateOut.focus();
}
this.dateOut.onClose(dateText, inst);
},
getStartDate: function() {
return this.dateIn.getSelectedDate();
},
getEndDate: function() {
return this.dateOut.getSelectedDate();
},
isStartDateValid: function() {
var dt = this.dateIn.getSelectedDate();
return this.isValidDate(dt);
},
isEndDateValid: function() {
var dt = this.dateOut.getSelectedDate();
return this.isValidDate(dt);
},
setDates: function(startDate, endDate) {
this.dateIn.setDate(startDate);
this.dateOut.setDate(endDate);
},
setMinMaxStartDate: function(minDate, maxDate) {
this.dateIn.setMinMaxDate(minDate, maxDate);
},
isDate : function(obj) {
///
/// Determines if the passed object is an instance of Date.
///
/// The object to test.
return Object.prototype.toString.call(obj) === '[object Date]';
},
isValidDate : function(obj) {
//
// Determines if the passed object is a Date object, containing an actual date.
//
// The object to test.
return this.isDate(obj) && !isNaN(obj.getTime());
}
};
Pcln.RCWidget = (function(pricelineUrl) {
// private variables
var datePair;
var instance = {};
var pricelineUrl = 'https://www.priceline.com';
// must implement htmlReady, isStartDateValid, isEndDateValid, getStartDate, getEndDate,
// START: called by the PclnRcCrossSell object, must be implemented
instance.getStartDate = function() {
return (datePair) ? datePair.getStartDate() : new Date();
};
instance.getEndDate = function() {
return (datePair) ? datePair.getEndDate() : new Date();
};
instance.isStartDateValid = function() {
return (datePair) ? datePair.isStartDateValid() : false;
};
instance.isEndDateValid = function() {
return (datePair) ? datePair.isEndDateValid() : false;
};
instance.setDates = function(startDate, endDate) {
if (datePair) {
datePair.setDates(startDate, endDate);
}
};
instance.spanSelectorChanged = function(selectItemJq) {
var selectedTxt = $('option:selected', selectItemJq).text();
$("span", selectItemJq.parent()).text(selectedTxt);
};
// First method called by the PclnRcCrossSell object
instance.htmlReady = function(data /* {} */) {
// create the class that manages date picking.
datePair = new Pcln.datePair($('#pickUpDate', data.parent), $('#dropOffDate', data.parent), {}, {}, {}, pricelineUrl);
var todaysDate = new Date();
datePair.setDates(data.startDate, data.endDate);
datePair.setMinMaxStartDate(todaysDate, datePair.addDays(todaysDate, 365));
// transfer the original data
Pcln.subscribeEvents([Pcln.InitialDisplayCompleted, Pcln.DisplayCompleted], function(event, eventData) {
// display the correct header depending on if it is Opaque or not.
var rateType= eventData.crossSellInstance.getRateTypeDisplayed();
if (rateType == Pcln.RateType.OPAQUE) {
$('#retailHeader', eventData.parent).hide();
$('#opaqueHeader', eventData.parent).show();
}
else {
$('#retailHeader', eventData.parent).show();
$('#opaqueHeader', eventData.parent).hide();
}
// display a warning if the we are not displaying in the requested currency and displaying USD pricing
if (!eventData.crossSellInstance.displayingInRequestedCurrency() && eventData.crossSellInstance.displayingUSDPricing()) {
$('#differentCurrencyNote').show();
}
else {
$('#differentCurrencyNote').hide();
}
});
// change the height of the iframe after the initial display has been completed.
Pcln.subscribeEvents([Pcln.NewFrameHeight], function(event, eventData) {
var newHgt = $('html').height();
// because I cant get the right height in IE8
if (!newHgt || newHgt < 100) {
newHgt = eventData.ieFallBackHgt;
}
newHgt += 'px';
iframeJQ = $("#pricelineIframe", window.parent.document.body);
iframeJQ.height(newHgt);
});
};
// END: function definitions required by PclnRcCrossSell object
// return the instance
return instance;
});
// Use the pattern below, just in case it is already created some place else
window.Pcln = (window.Pcln) ? window.Pcln : {};
/*
* Caller should pass the following parameters object to setOptions.
*
* {
* targetId - id that JQuery can use to place the cross sell html after
*
* accessToken and the following:
* latitude, longitude : - required fields to get the nearest airport.
* checkInDateTime :
* checkOutDateTime :
* or
* offerToken - in this case we can compute lat, long, checkInDate and checkOutDate
*
* Optional:
* refId : '???',
* refIdClickIdPrefix : '???',
* logger - function that will log information and/or errors
* currencyCode - optional for displaying in different currencies.
* bkgHotelId - hotel id, when accessToken only
* numberGuests - number of guest, when accessToken only
* gaCategory : category of all ga events sent
* gaErrorCategory : category of all ga event error events
*
* }
*/
Pcln.NewSearchResultsDisplayed = "NewSearchResultsDisplayed"; // after we have displayed new search results.
// Events that are published from this Module
Pcln.InitialDisplayCompleted = "InitialDisplayCompleted"; // the first time we are done displaying
Pcln.DisplayCompleted = "DisplayCompleted"; // second thru nth times that we are done displaying
Pcln.NewFrameHeight = "NewFrameHeight"; // after we change the content height, sends the new height.
Pcln.RateType = {
UNKNOWN : 0, // starts out as this, before rates are displayed
RETAIL : 1,
RCC : 2,
OPAQUE : 3,
get : function(rateTypeString) {
if (rateTypeString == "RCC") {
return this.RCC;
}
if (rateTypeString == "RETAIL") {
return this.RETAIL;
}
if (rateTypeString == "OPAQUE") {
return this.OPAQUE;
}
return this.UNKNOWN;
}
};
Pcln.CurrencyDisplay = {
POS_CURRENCY_CODE : "POS_CURRENCY_CODE",
CURRENCY_ABBR_HTML : "CURRENCY_ABBR_HTML" // default
};
Pcln.subscribeEvent = function(eventName, handler) {
$("body").bind(eventName, handler);
};
Pcln.subscribeEvents = function(eventNames, handler) {
$.each(eventNames, function( i, eventName ) {
Pcln.subscribeEvent(eventName, handler);
});
};
// RCCrossSell
// Singleton
Pcln.RcCrossSell = (function(template) {
// private variables
var instance = {};
var initialLoad = true;
var errorDisplayed = false;
var currentUrl = "";
var dataDoneLoading = false;
var scriptLoadTimedOut = false;
var rateType = Pcln.RateType.UNKNOWN; // Pcln.RateType
var displayCurrency = "";
var jsonpTimeoutHandle;
// get the url that serves all files and services.
var scriptSrc = $("#PclnRcCrossSellCSS").attr('href');
var index = scriptSrc.indexOf('.com');
var pricelineUrl = scriptSrc.substring(0, index + 4); // + 4 to get the .com
// create the rcApiService API which can only be accessed as secure in all cases
// eventually apc of the site running, could different ie: Mobile Web
var rcApiServiceUrl = pricelineUrl + "/pws/v0/drive/cross-sell.jsonp?apc=DESKTOP&callback=Pcln.crossSellInstance.dataLoaded"
if (rcApiServiceUrl.indexOf("http://") != -1) {
rcApiServiceUrl = rcApiServiceUrl.replaceAllIgnoreCase("http:", "https:");
}
// instance variables
// assign the default options
instance.options = {
maxRecommendedRatesDisplay : 3, // dont display more than 3 recommended deals unless the caller calls show all cars
gaCategory : 'widget',
gaErrorCategory : 'widgetError',
milliSecondsPerDay : 3600*24*1000,
isInternal : function() {
// indicates the usage is from within Priceline.
// governs the name of the ref ids when appended to urls, external needs ref-id and internal needs iref-id.
return (this.offerToken) ? true : false;
},
getCheckInDateTime : function() {
return (this.checkInDateTime instanceof Date == false) ? convertStrToDate(this.checkInDateTime) : this.checkInDateTime;
},
getCheckOutDateTime : function() {
return (this.checkOutDateTime instanceof Date == false) ? convertStrToDate(this.checkOutDateTime) : this.checkOutDateTime;
},
daysInBetween : function(to, from) {
return Math.abs(Math.floor( to.getTime() / this.milliSecondsPerDay) - Math.floor( from.getTime() / (this.milliSecondsPerDay)));
},
addDays : function(theDate, days) {
return new Date(theDate.getTime() + (days * this.milliSecondsPerDay));
},
adjustDatesInPast : function() {
var firstDate, lastDate, currentDate, daysInBet, firstHHMM, lastHHMM;
// adjust the dates if they are less than todays date, use the same number of days inbetween.
try {
firstDate = this.getCheckInDateTime();
lastDate = this.getCheckOutDateTime();
currentDate = new Date();
if (firstDate && firstDate < currentDate) {
var firstHHMM = {hh : firstDate.getHours(), mm : firstDate.getMinutes()};
var lastHHMM = {hh : lastDate.getHours(), mm : lastDate.getMinutes()};
daysInBet = this.daysInBetween(firstDate, lastDate);
firstDate = this.addDays(currentDate, 1);
lastDate = this.addDays(firstDate, daysInBet);
// use the original hours and days
firstDate.setHours(firstHHMM.hh);
firstDate.setMinutes(firstHHMM.mm);
lastDate.setHours(lastHHMM.hh);
lastDate.setMinutes(lastHHMM.mm);
this.setCheckInDateTime(firstDate);
this.setCheckOutDateTime(lastDate);
}
}
catch (e) {
}
},
setCheckInDateTime : function(checkInDateTime) {
this.checkInDateTime = (checkInDateTime instanceof Date == false) ? convertStrToDate(checkInDateTime) : checkInDateTime;
},
setCheckOutDateTime : function(checkOutDateTime) {
this.checkOutDateTime = (checkOutDateTime instanceof Date == false) ? convertStrToDate(checkOutDateTime) : checkOutDateTime;
},
// unless it is booking these should be overridden
refId : 'PLBOOKINGXSELL',
refIdClickIdPrefix : 'PLBOOKINGXSELL_', // will add a label to the end of this.
currencyDisplay : Pcln.CurrencyDisplay.CURRENCY_ABBR_HTML,
// caller should only override this when testing locally.
rcApiServiceUrl : rcApiServiceUrl,
// caller can override this if they want to perform custom logging.
logger : function() {
if (window.console && window.console.log) {
window.console.log(arguments);
}
}
};
// private methods
function loadGA(instance) {
// load analytics.js in this case because it can operate multiple trackers on a page.
// based on https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#multipletrackers
(function(i, s, o, g, r, a, m){
i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming.
// Creates an initial ga() function. The queued commands will be executed once analytics.js loads.
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments);
},
// Sets the time (as an integer) this tag was executed. Used for timing hits.
i[r].l = 1 * new Date();
// Insert the script tag asynchronously. Inserts above current tag to prevent blocking in
// addition to using the async attribute.
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-2975581-14', 'auto', {'name': 'pclnTracker', 'cookieDomain': 'priceline.com'}); // create a New tracker
ga('pclnTracker.send', 'pageview'); // Send page view for the cross sell.
};
function pad(val) {
return (val < 10) ? "0" + val : val;
}
function formatDate(dt /* JavaScript Date */) {
return "" + dt.getFullYear() + pad((dt.getMonth() + 1)) + pad(dt.getDate()) + "T" + pad(dt.getHours()) + ":" + pad(dt.getMinutes());
};
function formatDateMMDDYYYY(dt /* JavaScript Date */) {
return pad((dt.getMonth() + 1)) + "-" + pad(dt.getDate()) + "-" + dt.getFullYear();
};
function endWaitMode() {
if (errorDisplayed) {
$('#crossSellTemplateError').slideDown();
}
else {
$('#resultsDiv', instance.parent).slideDown();
}
$('#footerContainer', instance.parent).slideDown();
$('#refreshSearch', instance.parent).removeClass('disabled');
$(instance.parent).removeClass('disabled');
// the first time we have to remove the initial spinner, for simplicity we just always hide it
$('#initialWaitScreen').hide();
}
function startWaitMode() {
$('#crossSellTemplateError').slideUp();
$('#resultsDiv', instance.parent).slideUp();
$('#footerContainer', instance.parent).slideUp();
$('#refreshSearch', instance.parent).addClass('disabled');
$(instance.parent).addClass('disabled');
}
function callAsJSONP(url) {
startWaitMode();
// set this before the script tag, because it IE 8 this code below appears to be running synchronously.
dataDoneLoading = false;
scriptLoadTimedOut = false;
// go get the data, use JSON because it may be across domains.
var script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = currentUrl = url;
document.getElementsByTagName("head")[0].appendChild(script);
// add a timer so that we can quit after 15 seconds and just display the dumb banner
jsonpTimeoutHandle = setTimeout(instance.jsonpTimeout.bind(instance), 15000);
};
function allRatesFromOneAirport(recommendedRates, airports) {
var allAirportsAcrossRecRates = {};
if (recommendedRates) {
$.each(recommendedRates, function( i, rate ) {
if (i >= instance.options.maxRecommendedRatesDisplay) {
return true;
}
$.each(airports, function( airportCode, airport ) {
if (airport.rateLists) {
var recRatesByAirport = airport.rateLists.recommendedRates;
if ($.inArray( rate, recRatesByAirport) != -1) {
if (allAirportsAcrossRecRates[airportCode]) {
allAirportsAcrossRecRates[airportCode].count++;
}
else {
allAirportsAcrossRecRates[airportCode] = {count : 0};
}
return true;
}
}
});
});
}
if (allAirportsAcrossRecRates && Object.keys(allAirportsAcrossRecRates).length == 1) {
var airportCodeForRecRates = Object.keys(allAirportsAcrossRecRates)[0];
return airportCodeForRecRates;
}
return undefined;
};
function getSelectedAirport(airports) {
var locationPulldownJQ = $("#location", instance.parent);
// we either have multiple airports with a pulldown or a single airport as a label
if (locationPulldownJQ.is(':visible')) {
// after the original call, we have a selected airport, we should continue to use that airport if possible.
return $('option:selected', locationPulldownJQ).attr('value');
}
else {
var singleLocationJQ = $("#singleLocation", instance.parent);
return $('span', singleLocationJQ).attr('id');
}
};
function getRecommendedRates(airports) {
if (!airports) {
return undefined;
}
// the first time we just use the given recommended rates.
var recommendedRates;
if (initialLoad) {
recommendedRates = instance.crossSellData.crossSell.rateLists.recommendedRates;
}
else {
// after the original call, we have a selected airport, we should continue to use that airport if possible.
var selectedAirport = getSelectedAirport(airports);
var selectedAirportObj = airports[selectedAirport];
recommendedRates = (selectedAirportObj) ? selectedAirportObj.rateLists.recommendedRates : instance.crossSellData.crossSell.rateLists.recommendedRates;
}
return recommendedRates;
};
function exceptionToStr(err /* error object from catch */) {
var vDebug = "";
for (var prop in err)
{
vDebug += "property: "+ prop+ " value: ["+ err[prop]+ "]\n";
}
vDebug += "Value: [" + err.toString() + "]";
return vDebug;
}
/*
* This method can be executed with or without cross sell data, because we want to have a form that can still aid the user into getting results.
*/
function displayForm() {
// update the drop down with the new data.
//
var airports = (instance.crossSellData && instance.crossSellData.crossSell) ? instance.crossSellData.crossSell.airports : undefined;
var recommendedRates = getRecommendedRates(airports);
// if there is more than one show a pulldown, else show a label
var allAirportCodes = (airports) ? Object.keys(airports) : undefined;
var locationPulldownJQ = $("#location", instance.parent);
var singleLocationJQ = $("#singleLocation", instance.parent);
var airportCodeForRecRates;
if (allAirportCodes) {
$('#location-label', instance.parent).show();
if (allAirportCodes.length > 1) {
template.realizeHtmlFromClone(locationPulldownJQ, "locationOption", $("option", locationPulldownJQ), airports, true);
locationPulldownJQ.show();
$('#location-select-placeholder', instance.parent).show();
singleLocationJQ.hide();
}
else {
template.realizeHtmlFromClone(singleLocationJQ, "singleLocation", $("span", singleLocationJQ), airports[allAirportCodes[0]], false);
locationPulldownJQ.hide();
$('#location-select-placeholder', instance.parent).hide();
singleLocationJQ.show();
}
// select an item in the list only if all the recommended rates come from a single airport.
airportCodeForRecRates = allRatesFromOneAirport(recommendedRates, instance.crossSellData.crossSell.airports);
if (airportCodeForRecRates) {
$.each($('#location option'), function( j, option ) {
if (option.value == airportCodeForRecRates) {
$(this).attr("selected", "selected");
return true;
}
});
}
}
else {
// typically when there is an error response we dont want to show anything
$('#location-label', instance.parent).hide();
locationPulldownJQ.hide();
$('#location-select-placeholder', instance.parent).hide();
singleLocationJQ.hide();
}
// add all of the event handlers that are specific to the form
$("input", instance.parent).change(clearError);
$("select", instance.parent).change(clearError);
$(".clearFormError", instance.parent).click(clearError); // mostly for the calendar links and any thing else that needs a specific mark to clear errors.
// switch the results when the user selects a new location
$('#location', instance.parent).unbind("change");
$('#location', instance.parent).change(instance.airportChanged.bind(instance));
updateFormDateAndTime();
$('#searchDiv', instance.parent).show();
};
function getPartnerImage(currentRateType, allPartnerData, partnerCode) {
var defaultPartnerImage = (partnerCode) ? "//img1.priceline.com/rentalcars/logos/list/" + partnerCode + ".gif'" : undefined;
var partnerInfo = ((allPartnerData) && (partnerCode)) ? allPartnerData[partnerCode] : undefined;
if (currentRateType == Pcln.RateType.RETAIL) {
return ((partnerInfo) && (partnerInfo.images))
? partnerInfo.images['HEIGHT27']
: defaultPartnerImage;
}
if (currentRateType == Pcln.RateType.RCC) {
if (partnerInfo) {
if ((partnerInfo.images)) {
return partnerInfo.images['HEIGHT27'];
}
if (partnerInfo.rccImages) {
return partnerInfo.rccImages['SMALL'];
}
}
return defaultPartnerImage;
}
// for Opaque we do not show a partner image
return undefined;
};
function getPriceDisplay(priceDetails, ratePlan, posCurrencyCode, currencyAttrs) {
// determine the display price based on the following rules
// use the ratePlan to tell you which one you should use
// if there are no basePrices use the total.
var display = {price : "", currencyCode : "", totalAllInclusivePrice : priceDetails.totalAllInclusivePrice};
if (priceDetails.basePrices && ratePlan) {
display.price = priceDetails.basePrices[ratePlan];
}
else {
display.price = priceDetails.totalAllInclusivePrice;
}
// set the currency code that will be displayed
if (posCurrencyCode == "USD") {
display.currencyCode = "$";
}
else if (instance.options.currencyDisplay == Pcln.CurrencyDisplay.POS_CURRENCY_CODE) {
display.currencyCode = posCurrencyCode + " ";
}
else {
if (currencyAttrs) {
display.currencyCode = (currencyAttrs[posCurrencyCode]) ? currencyAttrs[posCurrencyCode].CURRENCY_ABBR_HTML[0]
: posCurrencyCode;
}
else {
display.currencyCode = (posCurrencyCode == "USD") ? "$" : posCurrencyCode;
}
}
// cutoff everything beyond the decimal point
display.price = Math.floor(display.price);
return display;
};
function displayResults() {
instance.allCarsAreDisplayed = true;
try {
function dVal(val) {
return (val) ? val : "";
}
var allRentalCars = $('#results', instance.parent);
allRentalCars.hide();
var airports = instance.crossSellData.crossSell.airports;
var recommendedRates = getRecommendedRates(airports);
var currencyAttrs = instance.crossSellData.crossSell.currencyAttributes; // will be undefined unless we requested a currency other than USD.
var allPartnerData = instance.crossSellData.crossSell.partners;
// display the recommended rates
if (recommendedRates) {
// build an array that combines the recommended rates, with the details for each rate.
var rates = instance.crossSellData.crossSell.rates;
var detailedRates = new Array();
$.each(recommendedRates, function( index, item ) {
try {
var rate = rates[item];
var vehicle = rate.vehicle;
var posCurrencyCode = rate.posCurrencyCode;
var priceDetails = rate.prices[posCurrencyCode];
var display = getPriceDisplay(priceDetails, rate.ratePlan, posCurrencyCode, currencyAttrs);
var peopleCapacity = dVal(vehicle.peopleCapacity);
var bagCapacity = dVal(vehicle.bagCapacity);
var vehicleExample = dVal(vehicle.vehicleExample);
// see the rate type being displayed, for now assume the rates all have the same rate type and currency
var currentRateType = Pcln.RateType.get(rate.type);
if (index == 0) {
rateType = currentRateType;
displayCurrency = posCurrencyCode;
}
var airportCounterTypeDisplay = "";
var onAirport = false;
if (rate.pickupLocation) {
airportCounterTypeDisplay = rate.pickupLocation.airportCounterTypeDisplay;
if (!airportCounterTypeDisplay) {
airportCounterTypeDisplay = "";
}
onAirport = rate.pickupLocation.onAirport;
}
// have to write the entire style otherwise it will not work in IE. IE strips the style when it just contains a var substuition. Like this: style="@displayMe@"
var displayMe = "style=''";
var hideMe = "style='display:none'";
var partnerImage = getPartnerImage(currentRateType, allPartnerData, rate.partnerCode);
var carImage = (vehicle.images['SIZE134X72']) ? vehicle.images['SIZE134X72'] : vehicle.images['134x74'];
// want to know if we were able to display all the cars because we want to offer See More Cars conditionally
if (index >= instance.options.maxRecommendedRatesDisplay) {
instance.allCarsAreDisplayed = false;
}
var vehicleHeading, displayVehicleExample;
if (vehicleExample == "") {
vehicleHeading = dVal(vehicle.vehicleDescription);
displayVehicleExample = "display:none";
}
else {
vehicleHeading = dVal(vehicle.vehicleDescription) + '-' + vehicleExample + ' or similar';
displayVehicleExample = "";
}
var rateSubstiutionObj = {
displayInitially : (index < instance.options.maxRecommendedRatesDisplay) ? displayMe : hideMe,
id : dVal(rate.id),
viewMoreLink : dVal(rate.links.DEFAULT),
carImageSrcTag : (carImage) ? "src='" + carImage + "'" : '',
displayVehicleExample : displayVehicleExample,
vehicleExample : vehicleExample,
vehicleDescription : dVal(vehicle.vehicleDescription),
vehicleHeading : vehicleHeading,
peopleCapacity : dVal(vehicle.peopleCapacity),
airConditioning : (vehicle.airConditioning) ? "Yes" : "No",
transmission : (vehicle.automaticTransmission) ? "Automatic" : "Manual",
currencyCode : dVal(display.currencyCode),
displayPrice : dVal(display.price),
totalAllInclusivePrice : dVal(display.totalAllInclusivePrice),
ratePlanDescr : (rate.ratePlanDisplay) ? "/ " + rate.ratePlanDisplay : "",
bagCapacity : dVal(vehicle.bagCapacity),
displayAutoTransmission : (vehicle.automaticTransmission) ? displayMe : hideMe,
displayManualTransmission : (vehicle.automaticTransmission) ? hideMe : displayMe,
displayPartnerLogo : (rate.partnerCode) ? displayMe : hideMe,
partnerLogoSrcTag : (partnerImage) ? "src='" + location.protocol + partnerImage + "'" : "",
displayAirConditioning : (vehicle.airConditioning) ? displayMe : hideMe,
displayUnlimited : (rate.rateDistance.unlimited) ? displayMe : hideMe,
displayAirportCounter : (airportCounterTypeDisplay == "") ? hideMe : displayMe,
airportCounterTypeDisplay : dVal(airportCounterTypeDisplay),
displayFreeCancellation : (rate.freeCancellation) ? displayMe : hideMe,
displayGreatValue : (rate.greatValue) ? displayMe : hideMe,
displayTotalColumn : (currentRateType == Pcln.RateType.OPAQUE) ? hideMe : displayMe,
displayTaxesAndFees : (currentRateType == Pcln.RateType.RCC) ? hideMe : displayMe,
displayLargePrice : (currentRateType == Pcln.RateType.RCC) ? hideMe : displayMe,
priceSuffix : (currentRateType == Pcln.RateType.RCC) ? "**" : "total",
displayPeopleCapacity : (peopleCapacity == "") ? hideMe : displayMe,
peopleCapacity : peopleCapacity,
displayBagCapacity : (bagCapacity == "") ? hideMe : displayMe,
bagCapacity : bagCapacity
};
detailedRates.push(rateSubstiutionObj);
}
catch (e) {
instance.sendGAErrorEvent({action : 'Error while displaying results', label: exceptionToStr(e)});
}
});
// display the each rate
if (detailedRates.length > 0) {
template.realizeHtmlFromClone(allRentalCars, "eachRentalCar", $(allRentalCars.children().get(0)), detailedRates, true);
}
}
// now that everything is realized we can show the results.
allRentalCars.show();
}
catch (e) {
instance.sendGAErrorEvent({action : 'DisplayResults', label: exceptionToStr(e)});
return false;
}
sendNewFrameHgtEvent(850);
return true;
};
function sendNewFrameHgtEvent(ieFallBackHgt) {
// do this on a timeout so that the correct height is sent
setTimeout(function() {
// send back to the iframe's parent for resizing.
// because I couldnt figure a way to get a height from IE8 and under
publishEvent(Pcln.NewFrameHeight, {ieFallBackHgt : ieFallBackHgt});
}, 1000);
};
function isErrorPage() {
return errorDisplayed;
};
function displayErrorPage() {
// when there is an error page seeing more cars makes no sense.
$('#seeMoreCars').css("visibility", 'hidden');
displayCompleted(true);
sendNewFrameHgtEvent(850);
};
function convertStrToDate(dt /* format is YYYYMMDDTHH:MM */) {
try {
return new Date(dt.substring(0,4),
(dt.substring(4,6) - 1),
dt.substring(6,8),
dt.substring(9,11),
dt.substring(12,14),
"0");
}
catch (e) {
instance.sendGAErrorEvent({action : 'convertStrToDate', label: dt});
return new Date();
}
};
function setTimeSelectors(timeSelector, val) {
$.each($('option', timeSelector), function( j, option ) {
if (option.value == val) {
$(option).attr("selected", "selected");
return true;
}
});
};
function updateFormDateAndTime() {
if (!instance.crossSellData || !instance.crossSellData.crossSell) {
return false;
}
// set the dates on the date picker
var pickupDateTime = convertStrToDate(instance.crossSellData.crossSell.pickupDateTime);
var returnDateTime = convertStrToDate(instance.crossSellData.crossSell.returnDateTime);
instance.dateHelper.setDates(pickupDateTime, returnDateTime);
// set the times
setTimeSelectors($('#pickUpTime', instance.parent), pad(pickupDateTime.getHours()) + ":" + pad(pickupDateTime.getMinutes()));
setTimeSelectors($('#dropOffTime', instance.parent), pad(returnDateTime.getHours()) + ":" + pad(returnDateTime.getMinutes()));
};
// Helper methods for publishing and subscribing to events.
function publishEvent(eventName, data) {
// to emuluate the async nature of true events, trigger the event on a timer. This allows the current
// JavaScript thread to complete and for this event to be handled later on its own thread. I believe this
// is good for the UI, giving it a chance to paint thereby giving the user a more responsive feel.
setTimeout(function() {
$("body").trigger(eventName, data);
}, 10);
}
function displayCompleted(dumbBanner) {
errorDisplayed = dumbBanner;
var eventName = (initialLoad) ? Pcln.InitialDisplayCompleted : Pcln.DisplayCompleted;
publishEvent(eventName, {crossSellInstance : instance, parent : instance.parent, dumbBanner : dumbBanner});
initialLoad = false;
instance.parent.show();
}
function display() {
$('#seeMoreCars').css("visibility", 'visible');
// check for all the ways things can go wrong first.
if (!instance.crossSellData || !instance.crossSellData.crossSell) {
instance.sendGAErrorEvent({action : 'Cross Sell Data not found in display method', label: ''});
displayForm();
displayErrorPage();
return false;
}
// check for an error response
var crossSellData = instance.crossSellData;
if (crossSellData.resultCode != 0 && crossSellData.resultCode != 200 && crossSellData.resultMessage) {
if (crossSellData.fieldValidationMessages && crossSellData.fieldValidationMessages.length >= 1) {
var resultMsg = crossSellData.resultMessage + ": " + crossSellData.fieldValidationMessages[0].description;
instance.sendGAErrorEvent({action : 'jsonpResponseValidationError', label: resultMsg});
}
else {
instance.sendGAErrorEvent({action : 'jsonpResponseError', label: crossSellData.resultMessage});
}
displayForm();
displayErrorPage();
return false;
}
displayForm();
if (displayResults() == false) {
displayErrorPage();
return false;
}
displayCompleted(false);
$('#seeMoreCars').css("visibility", 'visible');
// display See More Cars if there aren't any more than displayed
if (instance.allCarsAreDisplayed == true) {
$('#seeMoreCars').hide();
}
return true;
}
function combineDateTime(dt, tt) {
var returnDt = dt;
returnDt.setHours(tt.substring(0, 2));
returnDt.setMinutes(tt.substring(3, 5));
return returnDt;
};
function displayError(msg) {
$("#errorArea", instance.parent).text(msg);
};
function clearError() {
$("#errorArea", instance.parent).text("");
};
function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes*60000);
};
function checkDates() {
// check validity of the dates
if (instance.dateHelper.isStartDateValid() == false) {
$('#pickUpDate', instance.parent).focus();
displayError("Pick-Up Date is not a valid date.");
return false;
}
if (instance.dateHelper.isEndDateValid() == false) {
$('#dropOffDate', instance.parent).focus();
displayError("Drop-off Date is not a valid date.");
return false;
}
// make sure the start date and time is greater than the current date and time
var pickupDateTime=combineDateTime(instance.dateHelper.getStartDate(), $('#pickUpTime', instance.parent).val());
// make sure the end date is greater than the start date
var returnDateTime=combineDateTime(instance.dateHelper.getEndDate(), $('#dropOffTime', instance.parent).val());
if (pickupDateTime > returnDateTime) {
$('#pickUpDate', instance.parent).focus();
displayError("The Pick-Up date should be less than than the Drop-off date and time.");
return false;
}
return true;
};
function createJSONPUrl(initialSearch, options, pickupDateTime, returnDateTime /* JavaScript Date Objects */) {
var url = options.rcApiServiceUrl;
// when there is only an accessToken we need to also pass lat, long, and dates.
if (options.accessToken) {
url += "&pickup-location=" + options.latitude + "," + options.longitude;
url += "&access-token=" + options.accessToken;
// the first time we use check in and check out dates.
if (initialSearch) {
url += "&check-in-date-time=" + formatDate(pickupDateTime) + // form supplied or first time use the hotel checkin and checkout
"&check-out-date-time=" + formatDate(returnDateTime);
}
else {
url += "&pickup-date-time=" + formatDate(pickupDateTime) + // form supplied use the pickup and return date parameters.
"&return-date-time=" + formatDate(returnDateTime);
}
}
else {
// assuming I will have an offer token at this point. In this case the server can figure out all the other dates.
url += "&offer-token=" + options.offerToken;
// the subsequent calls need the pickup date and time
if (initialSearch == false) {
url += "&pickup-date-time=" + formatDate(pickupDateTime) + // form supplied use the pickup and return date parameters.
"&return-date-time=" + formatDate(returnDateTime);
}
}
// these are optional
if (options.currencyCode) {
url += "¤cy-code=" + options.currencyCode;
}
if (options.bkgHotelId) {
url += "&bkg-hotel-id=" + options.bkgHotelId;
}
if (options.bkgHotelId) {
url += "&number-of-guests=" + options.numberGuests;
}
return url;
};
// public methods
// Based on:
// https://developers.google.com/analytics/devguides/collection/analyticsjs/events
//
// gaObj can include the following fields.
// {category : ..., action : ..., label : ..., value : ...}
//
// only these signatures are valid:
// instance.sendGAEvent({action : 'act'});
// instance.sendGAEvent({action : 'act', label : 'label'});
// instance.sendGAEvent({action : 'act', label : 'label', value: must be a numeric value});
instance.sendGAEvent = function(gaObj) {
if (window.ga) {
ga('pclnTracker.send', 'event', this.options.gaCategory, gaObj.action, gaObj.label, gaObj.value);
this.options.logger(gaObj);
}
};
instance.sendGAErrorEvent = function(gaObj) {
if (window.ga) {
ga('pclnTracker.send', 'event', this.options.gaErrorCategory, gaObj.action, gaObj.label, gaObj.value);
this.options.logger(gaObj);
}
};
instance.getRateTypeDisplayed = function() {
return rateType;
};
instance.displayingInRequestedCurrency = function() {
return (this.options.currencyCode == displayCurrency) ? true : false;
};
instance.displayingUSDPricing = function() {
return ("USD" == displayCurrency) ? true : false;
};
instance.navigate = function(url, label, addRefs) {
this.sendGAEvent({action : 'click', label : label});
// on a timer so we can have time to send the GA event.
setTimeout(function() {
var appendParms = "";
if (addRefs) {
var appendParms = (url.indexOf("?") != -1) ? "&" : "?";
if (this.isInternal() == true) {
appendParms+= "irefid=" + this.refId + "&irefclickid=" + this.refIdClickIdPrefix + label.toUpperCase();
}
else {
appendParms+= "refid=" + this.refId + "&refclickid=" + this.refIdClickIdPrefix + label.toUpperCase();
}
}
window.open(url + appendParms, '_blank'); // depending on the browser setting it will be opened in a new window or new tab
}.bind(this.options), 100);
};
instance.simpleNavigate = function(label) {
var url = pricelineUrl + "/l/rental/cars.htm";
var travelStart, travelEnd, travelStartTime, travelEndTime;
if (checkDates()==false) {
return;
}
travelStart = formatDateMMDDYYYY(instance.dateHelper.getStartDate());
travelEnd = formatDateMMDDYYYY(instance.dateHelper.getEndDate());
travelStartTime = $('#pickUpTime', this.parent).val();
travelEndTime = $('#dropOffTime', this.parent).val();
url += "?ts=" + travelStart +
"&te=" + travelEnd +
"&ts_time=" + travelStartTime +
"&te_time=" + travelEndTime;
this.navigate(url, label, true);
};
instance.seeMore = function() {
if (checkDates()==false) {
return;
}
if (!instance.crossSellData || !instance.crossSellData.crossSell) {
instance.sendGAErrorEvent({action : 'Initial Load Missing Structure: See More', label: currentUrl});
return false;
}
// get the url out of the response
var airports = instance.crossSellData.crossSell.airports;
var selectedAirport = getSelectedAirport(airports);
var selectedAirportObj = airports[selectedAirport];
// add the refid and click id
if (selectedAirportObj.searchLink) {
this.navigate(selectedAirportObj.searchLink, "seeMore", true);
}
};
instance.showAllCars = function(options) {
$(".carResult", this.parent).show();
$("#seeMoreCars", this.parent).hide();
};
instance.setOptions = function(options) {
// allow the caller to overload any of the initialization properties.
if (options) {
$.extend(this.options, options);
this.options.adjustDatesInPast();
}
};
instance.jsonpTimeout = function() {
if (dataDoneLoading == false) {
scriptLoadTimedOut = true;
displayForm();
displayErrorPage();
endWaitMode();
this.sendGAErrorEvent({action : 'JSONP response did not return within the required period.', label: currentUrl});
}
};
instance.dataLoaded = function (crossSellData) {
try {
// if the response came back sooner than 15 seconds then display it.
if (scriptLoadTimedOut == false) {
// remove the current timer so it will no longer fire, this caused issues when the user performed Update within 15 seconds of the previous Update.
if (jsonpTimeoutHandle) {
clearTimeout(jsonpTimeoutHandle);
jsonpTimeoutHandle = undefined;
}
dataDoneLoading = true;
this.crossSellData=crossSellData;
display();
}
}
catch (e) {
this.sendGAErrorEvent({action : 'dataLoaded', label: exceptionToStr(e)});
}
endWaitMode();
};
instance.airportChanged = function() {
displayResults();
var locationPulldownJQ = $("#location", instance.parent);
var selectedAirport = $('option:selected', locationPulldownJQ).attr('value');
this.sendGAEvent({action : 'airportChanged', label : selectedAirport});
};
instance.refreshSearch = function() {
if (checkDates()==false) {
return;
}
// make sure it is not currently disabled
if (('#refreshSearch', this.parent).hasClass('disabled')) {
return;
}
clearError();
// format the date to get it ready to be a request parameter
var pickupDateTime=combineDateTime(instance.dateHelper.getStartDate(), $('#pickUpTime', this.parent).val());
var returnDateTime=combineDateTime(instance.dateHelper.getEndDate(), $('#dropOffTime', this.parent).val());
// re-search
this.search(pickupDateTime, returnDateTime);
$("#seeMoreCars", this.parent).show();
this.sendGAEvent({action : 'refreshSearch'});
};
instance.search = function(pickupDateTime, returnDateTime /* JavaScript Date Objects */) {
var url = createJSONPUrl(false, this.options, pickupDateTime, returnDateTime);
callAsJSONP(url);
};
instance.initialSearch = function(checkInDateTime, checkOutDateTime /* JavaScript Date Objects or strings */) {
var url = createJSONPUrl(true, this.options, checkInDateTime, checkOutDateTime);
callAsJSONP(url);
};
instance.init = function(dateHelper, /* this object must define htmlReady, isStartDateValid, isEndDateValid, getStartDate, getEndDate, setDates */
options) {
this.dateHelper = dateHelper;
this.setOptions(options);
$(document).ready(function() {
// put the html on the page
this.targetJQ = $(this.options.targetId);
this.targetJQ.after(this.crossSellTemplateText);
// dont need it anymore, might as well release the memory it is using.
this.crossSellTemplateText = "";
// record the parent for ease of use.
this.parent = $("#crossSellTemplate");
// add event handler onto refresh search
$('#refreshSearch', this.parent).click(this.refreshSearch.bind(this));
// callback to indicate the html is now on the page.
dateHelper.htmlReady({
parent : this.parent,
startDate : this.options.getCheckInDateTime(),
endDate : this.options.getCheckOutDateTime()
});
loadGA(instance);
// get the data for the widget.
this.initialSearch(this.options.getCheckInDateTime(), this.options.getCheckOutDateTime());
}.bind(instance));
};
/*
* Code assumes that this is the first thing that is called after the constructor.
*/
instance.setTemplateText = function(crossSellTemplateText) {
this.crossSellTemplateText = crossSellTemplateText;
};
// return the instance
return instance;
});
/*
* This class supports basic templates. This class is designed to take initial Dom elements,
* clone them,
* remove the initial dom element from the page
* substutite object values into the cloned element
* put the cloned element back onto the page.
*
*/
Pcln.Template = (function() {
// private variables
var instance = {};
var clones = {};
// private methods
function createHtml(srcText, replaceObj) {
// go thru all the fields of the replaceObj and exchange with the actual values.
if (replaceObj) {
$.each(replaceObj, function(attr, value) {
// make the tags matching case insensitive because depending on how tags are used, the loading of the html, turns the tag name all lower case.
// this typlically occurred with images tags where we create the entire src attriubute with the tag to avoid loading errors.
srcText = srcText.replaceAllIgnoreCase("@" + attr + "@", value);
});
}
return srcText;
};
function addHtmlElement(srcText, appendTargetJQ, replaceObj) {
var appendJQ = $(createHtml(srcText, replaceObj));
appendTargetJQ.append(appendJQ);
return appendJQ;
};
// realizeHtmlFromClone - method for creating html from a template and inserting values from an JS Object
// Use this method, performs behind the scenes saving and loading of the element being cloned
//
// ownerJQ - jquery item that is the top level of the html section being rendered
// cloneAttrName - unique id that is used to store the cloned item into clone cache
// cloneJqElem - will be saved, removed from the dom, dupped to represent each the obj's data in html and then appended to the owner JQ
// obj - object that contains the data that will be realized into the template.
// isList - should the passed object be considered a list of objects or a single object
// true - creates a dom element for each item in the list
// false - create only a single dom element
instance.realizeHtmlFromClone = function(ownerJQ, cloneAttrName, cloneJqElem, obj, isList) {
// first check the cache to see if we already loaded it.
var srcText = clones[cloneAttrName];
if (!srcText) {
if (cloneJqElem[0]) {
var preScrubbed = cloneJqElem[0].outerHTML;
// because outerhtml takes our tags that are in this form
and returns
// we just replace @="" with @ to keep things clean
srcText = clones[cloneAttrName] = preScrubbed.replaceAllIgnoreCase('@=""', '@');
}
else {
return;
}
}
// use this technique instead of .empty() because .empty() was causing issues in IE 8.
ownerJQ.children().remove();
// append the item after it is realized with the obj
if (isList) {
$.each(obj, function( index, data ) {
// add automatic data that is useful
data.count = index;
addHtmlElement(srcText, ownerJQ, data);
});
}
else {
addHtmlElement(srcText, ownerJQ, obj);
}
};
// return the instance
return instance;
});
//for IE where the bind method may not be present.
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal
// IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {
}, fBound = function() {
return fToBind.apply(this instanceof fNOP && oThis ? this
: oThis, aArgs.concat(Array.prototype.slice
.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
if (!Object.keys) {
Object.keys = function(o) {
var result = [];
for(var name in o) {
if (o.hasOwnProperty(name))
result.push(name);
}
return result;
};
}
if (!String.prototype.replaceAllIgnoreCase) {
String.prototype.replaceAllIgnoreCase = function(replace, with_this) {
return this.replace(new RegExp(replace, 'gi'),with_this);
};
}
// if the instance variable name is changed below, change the value of the callback parameter when calling the jsonp
window.Pcln.crossSellInstance = Pcln.RcCrossSell(new Pcln.Template());
$(document).ready(function() {
window.Pcln.crossSellInstance.setTemplateText('
Add a rental car
@vehicleHeading@†
@peopleCapacity@
@bagCapacity@
AUTO
MAN
AC
Unlimited Miles Free Cancellation @airportCounterTypeDisplay@