PageSourceSearch

https://www.georgiaaquarium.org/wp-content/plugins/tessitura-gai/js/utils.js?ver=20261001185940

js georgiaaquarium.org collected 2026-10-02 09:50:26 UTC 30,003 bytes, 837 lines download raw bytes

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.