// 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

'); });