1var isLocal = (window.location.search.indexOf('isLocal') !== -1); 2var inTNEW = (window.location.host.indexOf('tnhs') !== -1) || (window.location.host.indexOf('tickets.georgiaaquarium.org') !== -1); 3var apiBaseURI, apiBaseURIv2; 4var suppressDebug = false; 5// If we're on TNEW, we have to use absolute paths for the REST API, and those paths differ based on environment. 6if (inTNEW) { 7 // If we're forcing local, use local. If we're on live, use live. Otherwise, default to test. 8 if (isLocal) { 9 apiBaseURI = 'https://georgiaaquarium.local/wp-json/gai/v1/'; 10 apiBaseURIv2 = 'https://georgiaaquarium.local/wp-json/gai/v2/'; 11 } 12 else if (typeof env !== 'undefined' && env.live) { 13 apiBaseURI = 'https://www.georgiaaquarium.org/wp-json/gai/v1/'; 14 apiBaseURIv2 = 'https://www.georgiaaquarium.org/wp-json/gai/v2/'; 15 } 16 else { 17 apiBaseURI = 'https://test-georgia-aquarium.pantheonsite.io/wp-json/gai/v1/'; 18 apiBaseURIv2 = 'https://test-georgia-aquarium.pantheonsite.io/wp-json/gai/v2/'; 19 } 20 debug_log("[TNEW] API Base URI is ",apiBaseURI); 21} 22// We're on site, can use relative paths for the REST API. 23else { 24 apiBaseURI = '/wp-json/gai/v1/'; 25 apiBaseURIv2 = '/wp-json/gai/v2/'; 26 debug_log("[TNEW] API Base URI is relativized: ",apiBaseURI); 27} 28 29 30 31var datepickerLocale = { 32 days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 33 daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 34 daysMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 35 //daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 36 months: ['January','February','March','April','May','June', 'July','August','September','October','November','December'], 37 monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 38 today: 'Today', 39 clear: 'Clear', 40 dateFormat: 'mm/dd/yyyy', 41 timeFormat: 'hh:ii aa', 42 firstDay: 0 43}; 44 45var altDatepickerLocale = { 46 days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 47 daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 48 //daysMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 49 daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 50 months: ['January','February','March','April','May','June', 'July','August','September','October','November','December'], 51 monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 52 today: 'Today', 53 clear: 'Clear', 54 dateFormat: 'mm/dd/yyyy', 55 timeFormat: 'hh:ii aa', 56 firstDay: 0 57}; 58 59function formToJSON(elements) { return elements.reduce(function(data,cv){ data[cv.name] = cv.value; return data; },{}); } 60 61function definedOrNull(thing){ 62 return ((typeof thing !== 'undefined') && (thing != 'undefined') ? thing : null); 63} 64 65function urlParam(name) { 66 return decodeURI( 67 (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||['',''])[1] 68 ); 69} 70 71function failOnPurpose() { 72 return (!urlParam('fail')); 73} 74 75function setCookie(name,value,seconds,noencode = false,domain = '') { 76 var expires = ""; 77 if (seconds) { 78 var date = new Date(); 79 date.setTime(date.getTime() + (seconds*1000)); 80 expires = "; expires=" + date.toUTCString(); 81 } 82 //console.log("Here's our cookie: ",name + "=" + (encodeURIComponent(value) || "") + expires + ";" + domain + "path=/"); 83 if (noencode){ 84 document.cookie = name + "=" + (value || "") + expires + "; " + (domain ? `domain=${domain};` : '') + "path=/"; 85 } 86 else { 87 document.cookie = name + "=" + (encodeURIComponent(value) || "") + expires + "; " + (domain ? `domain=${domain};` : '') + "path=/"; 88 } 89 //console.log("Stored",name,"as",encodeURIComponent(value)); 90} 91 92function getCookie(name) { 93 var nameEQ = name + "="; 94 var ca = document.cookie.split(';'); 95 for(var i=0;i < ca.length;i++) { 96 var c = ca[i]; 97 while (c.charAt(0)==' ') c = c.substring(1,c.length); 98 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 99 } 100 return null; 101} 102
103function eraseCookie(name) { 104 setCookie(name,"",-1); 105} 106 107function convertDate(date) { 108 return date.toLocaleString('en-US', { 109 month: 'numeric', 110 day: 'numeric', 111 year: 'numeric', 112 }).replace(/[^ -~]/g,''); 113} 114 115function convertDateToo(date) { 116 return date.toLocaleString('en-US', { 117 month: 'long', 118 day: 'numeric', 119 year: 'numeric', 120 }).replace(/[^ -~]/g,''); 121} 122 123function makePrice(price) { 124 return '$' + price.toFixed(2); 125} 126 127function makePriceNoCents(price) { 128 return '$' + parseInt(price); 129} 130 131 132function clearError($x){ 133 $x.text(''); 134} 135 136function showError($x,s,et) { 137 let msg = 'The online ticketing service has encountered an error. Please try again in a few minutes or select another date/time for your visit.'; 138 //console.error("Error", s, et); 139 switch(s) { 140 case "timeout": 141 msg = 'The network timed out. Please try again in a few moments.' 142 break; 143 case "error": 144 break; 145 case "abort": 146 break; 147 case "parsererror": 148 break; 149 case "custom": 150 msg = et; // We provide this, not a call. 151 break; 152 } 153 jQuery($x).html(msg); 154} 155 156function safeJSONParse(data) { 157 var parsed; 158 if (typeof data == 'object') { 159 debug_log('safeJSONParse: was given an object, maybe don\'t need to call this?'); 160 return data; 161 } 162 163 try { 164 parsed = JSON.parse(data); 165 } catch (e) { 166 parsed = { 167 status: 'error', 168 message: 'unparseable response' 169 }; 170 } 171 return parsed; 172} 173 174function addBatchItemsToCart(lineItems,$myError,$myLoader,successCallback,failureCallback,clearCartFirst = false){ 175 if (!jQuery.isArray(lineItems) || (lineItems.length < 1)) { 176 showError($myError,'custom','Please select one or more items to add.'); 177 //$myLoader.addClass('is-hidden'); 178 return false; 179 } 180 $myLoader.removeClass('is-hidden'); 181 let errorHappened = false; 182 183 // First establish our session. 184 let sess = Tession.assertSessionKey( 185 false, 186 function(d,s){ 187 debug_log("Assertion success, attempting cart add..."); 188 let uri = apiBaseURI + 'addToTessCart', 189 fields = { 190 lineItems: lineItems, 191 clearCartFirst: clearCartFirst 192 }; 193 194 //console.log("URI ",uri); 195 //console.log("FIELDS ",fields); 196 //console.log("SESSIONKEY",getCookie(Tession.PANTHEON_COOKIE)); 197 198 jQuery.ajax({ 199 type: "POST", 200 url: uri, 201 data: JSON.stringify(fields), 202 //async: false, // Just to keep things orderly for now. 203 contentType: 'application/json', 204 timeout: 60000, // 60 seconds 205 dataType: 'json', 206 beforeSend: function() { 207 clearError($myError); 208 }, 209 success: function (data, status) { 210 debug_log("Adding to cart", status, data); 211 let resp = safeJSONParse(data); 212 debug_log("Parsed",resp); 213 if (resp.status === 'success' && (env.live || !(getURLParameter('enableFake')) || !confirm("Fake fail ATC?"))) { 214 trackEvent('cart_add',{ 215 data: lineItems, 216 }); 217 218 // Analytics must never block the cart redirect: if addEEC 219 // throws, swallow it so successCallback (the navigation to 220 // the cart) still runs and t.busy is not left stuck true. 221 try { addEEC(lineItems); } catch (eec) { debug_log('[GA4 EE] addEEC error (non-blocking):', eec); } 222 if (typeof successCallback == 'function') { 223 // Add items to the datalayer for EEC. 224 //if (confirm("Proceed with success callback?")) 225 successCallback(resp); 226 } else { //if (confirm("Navigate to precart?")){ 227 Tession.goToTNEW(Tession.SSURL() + "components/prec
227art?p=1"); 228 } 229 } else { 230 pushException('addBatchToCart-errorResponse'); 231 showError($myError,resp.status,resp.message); 232 jQuery('.loader-wrapper').addClass('is-hidden'); 233 if (typeof failureCallback == 'function') { 234 failureCallback(resp); 235 } 236 //console.log(resp); 237 errorHappened = true; 238 } 239 }, 240 error: function (jqXHR, status, errorThrown) { 241 pushException('addBatchToCart-badResponse'); 242 showError($myError,status,"Failure of AddToCart: " + errorThrown); 243 jQuery('.loader-wrapper').addClass('is-hidden'); 244 if (typeof failureCallback == 'function') { 245 failureCallback(); 246 } 247 errorHappened = true; 248 }, 249 complete: function (jqXHR, status) { 250 //console.log("Complete", status); 251 } 252 }); 253 }, 254 function(d,s){ 255 // Error trying to assert the session. 256 pushException('addBatchToCart-noSessAssert'); 257 showError($myError,'error','Failed to assert session.'); 258 if (typeof failureCallback == 'function') { 259 failureCallback(); 260 } 261 jQuery('.loader-wrapper').addClass('is-hidden'); 262 errorHappened = true; 263 } 264 ); 265 266 267 // This is where we require a session or we cannot proceed. 268 // The RESTful call will attempt to grab one if the cookie is not 269 // present. That has the possibility to fail. 270 271 // when validation completes, we can then make the AJAX call. 272 //let validate = Tession.validateSessionKey(); 273 //console.log("Results of validate: ",validate); 274 275 /*jQuery.ajax({ 276 type: "POST", 277 url: uri, 278 data: JSON.stringify(fields), 279 //async: false, // Just to keep things orderly for now. 280 contentType: 'application/json', 281 timeout: 30000, // 30 seconds 282 dataType: 'json', 283 beforeSend: function() { 284 clearError($myError); 285 $myLoader.removeClass('is-hidden'); 286 }, 287 success: function (data, status) { 288 console.log("Adding to cart", status); 289 let resp = safeJSONParse(data); 290 if (resp.status == 'success') { 291 if (typeof successCallback == 'function') { 292 successCallback(resp); 293 } else { 294 window.location.href="/cart"; 295 } 296 } else { 297 showError($myError,resp.status,resp.message); 298 console.log(resp); 299 } 300 }, 301 error: function (jqXHR, status, errorThrown) { 302 showError($myError,status,errorThrown); 303 }, 304 complete: function (jqXHR, status) { 305 console.log("Complete", status); 306 jQuery('.loader-wrapper').addClass('is-hidden'); 307 } 308 });*/ 309 310 return !errorHappened; 311} 312 313// Massage our batch item add into EEC-friendly format. 314/** 315 * Build and push the GA4 `add_to_cart` event for a batch of cart line items. 316 * Called from addBatchItemsToCart() on a successful add â the single choke point 317 * covering every cart-addition flow on the site (admission, passholder, parking, 318 * combo meals, experiences, special admission, exp-first, dive video, events, 319 * bookings, memberships, gift certificates, donations, post-dive merchandise, 320 * "name a jelly"). 321 * 322 * All GA4 mapping, PII scrubbing, value calc, the ecommerce-clear and the data 323 * layer push live in the shared gaiEcommerce module (js/ga4-ecommerce.js) so the 324 * rest of the GA4 ecommerce events reuse identical logic. This just adapts the 325 * raw Tessitura line items to that helper. 326 */ 327function addEEC(items) { 328 if (!Array.isArray(items)) { return; } 329 // A flow that fires add_to_cart per item (e.g. the genad wizard, per the 330 // "each add is an add_to_cart" spec decision) sets this flag to suppress this batched push 331 // at the final cart commit, so items aren't counted twice. One-shot: reset here. 332 if (window.__gaiSkipBatchAddToCart) { window.__gaiSkipBatchAddToCart = false; return; } 333 if (typeof gaiEcommerce === 'undefined') { 334 debug_log('[GA4 EE] gaiEcommerce module not loaded; skipping add_to_cart push.'); 335 return; 336 } 337 let ga4Items = items.map(function (item) { return gaiEcommerce.mapLineItem(item); }
337).filter(Boolean); 338 gaiEcommerce.push('add_to_cart', ga4Items, { 339 currency: gaiEcommerce.CURRENCY, 340 value: gaiEcommerce.sumValue(ga4Items) 341 }); 342} 343 344/** 345 * Single choke point for GA4 exception (error) reporting â every purchase flow 346 * and AJAX error/bad-response handler across the plugin calls this instead of 347 * touching gaiEcommerce directly, so the "is gaiEcommerce loaded yet" guard 348 * lives in exactly one place. Same gaiEcommerce-missing handling as addEEC() 349 * above (debug_log instead of a silent no-op) rather than each of the ~200 350 * call sites repeating (and having to remember) the same inline guard. 351 * @param {string} description e.g. 'getOrderDetails-errorResponse' 352 * @param {Object} [opts] passed through to gaiEcommerce.pushException (e.g. {fatal:true}) 353 */ 354function pushException(description, opts) { 355 if (typeof gaiEcommerce === 'undefined') { 356 debug_log('[GA4 EE] gaiEcommerce module not loaded; skipping exception push:', description); 357 return; 358 } 359 gaiEcommerce.pushException(description, opts); 360} 361 362function getRandom(arr, n) { 363 var result = new Array(n), 364 len = arr.length, 365 taken = new Array(len); 366 if (n > len) 367 throw new RangeError("getRandom: more elements taken than available"); 368 while (n--) { 369 var x = Math.floor(Math.random() * len); 370 result[n] = arr[x in taken ? taken[x] : x]; 371 taken[x] = --len in taken ? taken[len] : len; 372 } 373 return result; 374} 375 376function getURLParameter(name) { 377 name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 378 var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 379 var results = regex.exec(location.search); 380 return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' ')); 381} 382 383function scrollTo(target,options) { 384 var defaults = { 385 delay: 0, 386 offset: 120, 387 speed: 500, 388 complete: function(){}, 389 }; 390 var actual = jQuery.extend({},defaults,options || {}); 391 392 if ((typeof target === 'object') || ((typeof target === 'string') && (target !== '#'))) { 393 debug_log("Target offset is ",jQuery(target).offset().top); 394 debug_log("Passed in offset is ",actual.offset); 395 debug_log("Final offset is ",jQuery(target).offset().top - actual.offset); 396 jQuery('html, body').delay(actual.delay).animate({scrollTop: jQuery(target).offset().top - actual.offset}, actual.speed,actual.complete); 397 } 398} 399 400//https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript 401/*const numberWithCommas = (x) => { 402 return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 403};*/ 404 405function numberWithCommas(x) { 406 return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 407}; 408 409 410function TossMyCookies() { 411 console.log("Here are your session cookies:"); 412 console.log("SESSION KEY"); 413 console.log(decodeURIComponent(getCookie(Tession.PANTHEON_COOKIE))); 414 console.log("CONSTITUENT ID",getCookie(Tession.CID_COOKIE)); 415 console.log("MODE OF SALE",getCookie(Tession.MOS_COOKIE)); 416 console.log("SOURCE",getCookie(Tession.SOURCE_COOKIE)); 417 console.log("LOGIN NAME",getCookie(Tession.NAME_COOKIE)); 418 console.log("EMAIL",getCookie(Tession.EMAIL_COOKIE)); 419 console.log("CART?",getCookie(Tession.CART_COOKIE)); 420 return; 421} 422 423function makeClassSafe(name) { 424 return encodeURIComponent( 425 name.toLowerCase() 426 ).replace(/%[0-9A-F]{2}/gi,''); 427} 428 429function IE11ConvertDate(dateString) { 430 // Presumes a format of MM/DD/YYYY 431 // Did you know that IE11 inserts secret invisible characters into strings? It's true! 432 // https://stackoverflow.com/questions/30783955/strange-behaviour-with-spliting-a-string-in-javascript 433 //var splits = dateString.replace(/[^\x00-\x7F]/g, "").split("/"); 434 let splits = dateString.match(/\d+/g); 435 let newDate = new Date(Date.UTC(parseInt(splits[2]),parseInt(splits[0]) - 1, parseInt(splits[1]))); 436 return newDate; 437} 438 439//IE11 polyfill 440if (!Array.from) { 441 Array.from = (function () { 442 var toStr = Object.prototype.toString; 443 var isCallable = function (fn) { 444 return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; 445 }; 446 var toInteger = function (value) { 447 var number = Number(value); 448 if (isNaN(number)) { return 0; } 449 if (number === 0 || !isFinite(number)) { return number; } 450 return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); 451 }; 452 var maxSafeInteger = Math.pow(2, 53) - 1; 453 var toLength = function (value) { 454 var len = toInteger(value); 455 return Math.min(Math.max(len, 0), maxSafeInteger); 456 }; 457 458 // The length property of the from method is 1. 459 return function from(arrayLike/*, mapFn, thisArg */) { 460 // 1. Let C be the this value. 461 var C = this; 462 463 // 2. Let items be ToObject(arrayLike). 464 var items = Object(arrayLike); 465 466 // 3. ReturnIfAbrupt(items). 467 if (arrayLike == null) { 468 throw new TypeError('Array.from requires an array-like object - not null or undefined'); 469 } 470 471 // 4. If mapfn is undefined, then let mapping be false. 472 var mapFn = arguments.length > 1 ? arguments[1] : void undefined; 473 var T; 474 if (typeof mapFn !== 'undefined') { 475 // 5. else 476 // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. 477 if (!isCallable(mapFn)) { 478 throw new TypeError('Array.from: when provided, the second argument must be a function'); 479 } 480 481 // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. 482 if (arguments.length > 2) { 483 T = arguments[2]; 484 } 485 } 486 487 // 10. Let lenValue be Get(items, "length"). 488 // 11. Let len be ToLength(lenValue). 489 var len = toLength(items.length); 490 491 // 13. If IsConstructor(C) is true, then 492 // 13. a. Let A be the result of calling the [[Construct]] internal method 493 // of C with an argument list containing the single item len. 494 // 14. a. Else, Let A be ArrayCreate(len). 495 var A = isCallable(C) ? Object(new C(len)) : new Array(len); 496 497 // 16. Let k be 0. 498 var k = 0; 499 // 17. Repeat, while k < len⦠(also steps a - h) 500 var kValue; 501 while (k < len) { 502 kValue = items[k]; 503 if (mapFn) { 504 A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); 505 } else { 506 A[k] = kValue; 507 } 508 k += 1; 509 } 510 // 18. Let putStatus be Put(A, "length", len, true). 511 A.length = len; 512 // 20. Return A. 513 return A; 514 }; 515 }()); 516} 517 518//IE11 polyfill
519// https://tc39.github.io/ecma262/#sec-array.prototype.find 520if (!Array.prototype.find) { 521 Object.defineProperty(Array.prototype, 'find', { 522 value: function(predicate) { 523 // 1. Let O be ? ToObject(this value). 524 if (this == null) { 525 throw new TypeError('"this" is null or not defined'); 526 } 527 528 var o = Object(this); 529 530 // 2. Let len be ? ToLength(? Get(O, "length")). 531 var len = o.length >>> 0; 532 533 // 3. If IsCallable(predicate) is false, throw a TypeError exception. 534 if (typeof predicate !== 'function') { 535 throw new TypeError('predicate must be a function'); 536 } 537 538 // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 539 var thisArg = arguments[1]; 540 541 // 5. Let k be 0. 542 var k = 0; 543 544 // 6. Repeat, while k < len 545 while (k < len) { 546 // a. Let Pk be ! ToString(k). 547 // b. Let kValue be ? Get(O, Pk). 548 // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 549 // d. If testResult is true, return kValue. 550 var kValue = o[k]; 551 if (predicate.call(thisArg, kValue, k, o)) { 552 return kValue; 553 } 554 // e. Increase k by 1. 555 k++; 556 } 557 558 // 7. Return undefined. 559 return undefined; 560 }, 561 configurable: true, 562 writable: true 563 }); 564} 565 566function objectify(arr, keycolumn, valuecolumn) { 567 return arr.reduce(function(p,c) { 568 if (keycolumn in c) { // Must have the key column 569 if (valuecolumn) { // If we requested a value column 570 if (valuecolumn in c) { // And the current element has it 571 p[c[keycolumn]] = c[valuecolumn]; 572 } 573 } 574 else { 575 // If no value column was requested, we're simply converting this 576 // to object properties keyed by the key column. 577 p[c[keycolumn]] = c; 578 } 579 } 580 return p; 581 },{}); 582 583} 584 585// Returns TRUE if the current time (ET) is later than the reported closing time, which we use to adapt ticket display/sales. 586function tooLate() { 587 let date = Date.now(); 588 let todayCutOff = (jQuery('#closeTS').val() * 1000); 589 debug_log("[BHZ] TS now",date,"today's close TS",todayCutOff); 590 return (date > todayCutOff); 591} 592 593function isQuarantined(date) { 594 return ( 595 ((date.getFullYear() === 2020) && 596 ( 597 (date.getMonth() < 5) || 598 ((date.getMonth() === 5) && (date.getDate() < 15)) || 599 ((date.getMonth() >= 8)) 600 )) || (date.getFullYear() >= 2021) 601 ); 602} 603 604function isMomentQuarantined(date) { 605 return ( 606 ((date.year() === 2020) && 607 ((date.month() < 5) || 608 ((date.month() === 5) && (date.date() < 15)) || 609 ((date.month() >= 8)) 610 )) || (date.year() >= 2021) 611 ); 612} 613 614function isReserveQuarantined(date) { 615 return ( 616 ((date.getFullYear() === 2020) && 617 ( 618 (date.getMonth() < 5) || 619 ((date.getMonth() === 5) && (date.getDate() < 13)) || 620 ((date.getMonth() >= 8)) 621 )) || (date.getFullYear() >= 2021) 622 ); 623} 624 625function isReserveMomentQuarantined(date) { 626 return ( 627 ((date.year() === 2020) && 628 ((date.month() < 5) || 629 ((date.month() === 5) && (date.date() < 13)) || 630 ((date.month() >= 8)) 631 )) || (date.year() >= 2021) 632 ); 633} 634 635function useOriginal(date) { 636 //return false; 637 return ( 638 ((date.year() === 2020) && ( (date.month() >= 6)) ) || 639 (date.year() >= 2021) 640 ); 641} 642 643function useOtherOriginal(date) { 644 //return false; 645 return ( 646 ((date.getFullYear() === 2020) && ( (date.getMonth() >= 6)) ) || 647 (date.getFullYear() >= 2021) 648 ); 649} 650 651 652function failForwardFunction(callback, opt_timeout) { 653 var called = false; 654 function fn() { 655 if (!called) { 656 called = true; 657 callback(); 658 } 659 } 660 setTimeout(fn, opt_timeout || 1000); 661 return fn; 662} 663 664function debug_log() { 665 if ((window.location.host.indexOf('georgiaaquarium.org') == -1) || (window.location.search.indexOf('debug=1') !== -1)) { 666 console.log.apply(this,arguments); 667 } 668} 669 670function debug_warn() { 671 if ((window.location.host.indexOf('georgiaaquarium.org') == -1) || (window.location.search.indexOf('debug=1') !== -1)) { 672 console.warn.apply(this,arguments); 673 } 674} 675 676function debug_error() { 677 if ((window.location.host.indexOf('georgiaaquarium.org') == -1) || (window.location.search.indexOf('debug=1') !== -1)) { 678 console.error.apply(this,arguments); 679 } 680} 681 682function truncificate(x,d) { 683 var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (d|| -1) + '})?'); 684 return x.toString().match(re)[0]; 685} 686 687const stripTrailingSlash = (str) => { 688 return str.endsWith('/') ? 689 str.slice(0, -1) : 690 str; 691}; 692 693const base64UrlEncode = (str) => { 694 return btoa(str) 695 .replace(/\+/g, '-') 696 .replace(/\//g, '_') 697 .replace(/=+$/, ''); 698}; 699 700const base64UrlDecode = (str) => { 701 str = str 702 .replace(/-/g, '+') 703 .replace(/_/g, '/'); 704 while (str.length % 4) { 705 str += '='; 706 } 707 return atob(str); 708} 709 710var supportTouch = jQuery.support.touch, 711 scrollEvent = "touchmove scroll", 712 touchStartEvent = supportTouch ? "touchstart" : "mousedown", 713 touchStopEvent = supportTouch ? "touchend" : "mouseup", 714 touchMoveEvent = supportTouch ? "touchmove" : "mousemove"; 715 716(function($){ 717 $.event.special.swipeupdown = { 718 setup: function() { 719 var thisObject = this; 720 var $this = $(thisObject); 721 $this.bind(touchStartEvent, function(event) { 722 var data = event.originalEvent.touches ? 723 event.originalEvent.touches[ 0 ] : 724 event, 725 start = { 726 time: (new Date).getTime(), 727 coords: [ data.pageX, data.pageY ], 728 origin: $(event.target) 729 }, 730 stop; 731 732 function moveHandler(event) { 733 if (!start) { 734 return; 735 } 736 var data = event.originalEvent.touches ? 737 event.originalEvent.touches[ 0 ] : 738 event; 739 stop = { 740 time: (new Date).getTime(), 741 coords: [ data.pageX, data.pageY ] 742 }; 743 744 // prevent scrolling 745 if (Math.abs(start.coords[1] - stop.coords[1]) > 10) { 746 event.preventDefault(); 747 } 748 } 749 $this 750 .bind(touchMoveEvent, moveHandler) 751 .one(touchStopEvent, function(event) { 752 $this.unbind(touchMoveEvent, moveHandler); 753 if (start && stop) { 754 if (stop.time - start.time < 1000 && 755 Math.abs(start.coords[1] - stop.coords[1]) > 30 && 756 Math.abs(start.coords[0] - stop.coords[0]) < 75) { 757 start.origin 758 .trigger("swipeupdown") 759 .trigger(start.coords[1] > stop.coords[1] ? "swipeup" : "swipedown"); 760 } 761 } 762 start = stop = undefined; 763 }); 764 }); 765 } 766 }; 767 $.each({
768 swipedown: "swipeupdown", 769 swipeup: "swipeupdown" 770 }, function(event, sourceEvent){ 771 $.event.special[event] = { 772 setup: function(){ 773 $(this).bind(sourceEvent, $.noop); 774 } 775 }; 776 }); 777})(jQuery); 778 779 780 781/** 782 * Fetches geolocation headers from Pantheon CDN 783 * @returns {Promise<Object>} Object containing geolocation data 784 */ 785function getPantheonHeader() { 786 return new Promise((resolve, reject) => { 787 fetch(document.location.href) 788 .then(response => { 789 // Construct standard geolocation object from response headers 790 const headerValue = { 791 ip: response.headers.get("x-client-ip") || response.headers.get("x-forwarded-for"), 792 geo: { 793 city: response.headers.get("x-geo-city"), 794 country: response.headers.get("x-geo-country-name"), 795 countryCode: response.headers.get("x-geo-country-code"), 796 region: response.headers.get("x-geo-region"), 797 postalCode: response.headers.get("x-geo-postal-code"), 798 } 799 }; 800 801 debug_log('[PANTHEON] Headers fetched:', headerValue); 802 resolve(headerValue); 803 }) 804 .catch(error => { 805 console.error('[PANTHEON] Error fetching headers:', error); 806 reject(error); 807 }); 808 }); 809} 810 811/** 812 * Fetches geolocation headers with session storage caching 813 * @returns {Promise<Object>} Cached or fresh geolocation data 814 */ 815async function getHeaderResponse() { 816 const cacheKey = 'pantheonHeader'; 817 const cachedValue = sessionStorage.getItem(cacheKey); 818 819 if (cachedValue) { 820 debug_log('[PANTHEON] Returning cached header data'); 821 try { 822 return JSON.parse(cachedValue); 823 } catch (e) { 824 debug_error('[PANTHEON] Cache parse error:', e); 825 } 826 } 827 828 try { 829 const headerValue = await getPantheonHeader(); 830 sessionStorage.setItem(cacheKey, JSON.stringify(headerValue)); 831 debug_log('[PANTHEON] Header data cached for session'); 832 return headerValue; 833 } catch (error) { 834 console.error('[PANTHEON] Error getHeaderResponse:', error); 835 throw error; 836 } 837}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.