1/** 2 * Gel 2022.0 3 * Primary Public External JS File 4 * @version 2022.0.3 r 1 5 * @created 2013-11-13 by Fritz Green 6 * @modified 2023-07-24 by Christina Joly 7 */ 8 9/** 10 * Global variables (Additional global variables are defined in core resources.) 11 */ 12 13/** 14 * jQuery default animation settings 15 */ 16var hide_time = 600, hide_easing = "easeInOutQuart", show_time = 460, show_easing = "swing", dfFx = "slide", dfFxOpts = {direction:'up'}, dfFxDur = 300; 17 18/** 19 * Temp Overlay Message Time 20 * @type {number} 21 */ 22var tempMessageTime = 3500, tomct = 0, message = []; 23 24/** 25 * Gel Product vars 26 */ 27var $atc_form, $atc_response, $hcart; 28 29/** 30 * Kaptchu Image Refresh Counter 31 * @type {number} 32 */ 33var invis_counter = 1; 34 35/** 36 * Global Form Highlight Color 37 * @type {string} 38 */ 39var highlight_color = "#F5E4A0"; 40 41/** 42 * Overlay Modal Globals - Used for temporarily storing overlayHandler() variables that need to be persistent. 43 * @type {Array} 44 */ 45var olGlbls = []; 46 47/** 48 * Window Width Variables 49 * @type {*|jQuery} 50 */ 51var $ww, $mmw, $mnt; 52 53/** 54 * Scripts to execute when page is loaded and ready using jQuery page ready callback function 55 */ 56$(function(){ 57 58 // Common Element Selector References 59 ftr = $('footer'); 60 hdr = $('header'); 61 stt = $('.back-to-top'); 62 $ww = $(window).width(); 63 $mmw = ftr.attr("data-stop-menu-width"); 64 $mnt = $('.nav-trigger'); 65 66 // Set the base url and subdirectory path variables. 67 base_url = ftr.attr('data-base-url'); 68 subdir_path = ftr.attr('data-subdir-path'); 69 70 // Set ecommerce values to global variables. 71 $atc_form = $('#add_to_cart'); 72 $atc_response = $('.add-to-cart-response'); 73 $hcart = $('.header-cart'); 74 75 // Handle Enabling/Disabling of Main Nav Menu jQuery Animation. 76 // Note: Mobile menu display utilizes addToggleLinkListeners() and not these Nav Listeners. 77 addNavListeners('.nav-level-1'); 78 79 // Mitigate IE/Edge list-item=none render bug on list items hidden when page is initially loaded. 80 edgeHiddenListsFix(); 81 82 // Obfuscate Email Addresses 83 obfuscateEmails(); 84 85 // Google GTAG/Analytics Event Tracking 86 addGtagEventListeners(); 87 88 // Add Window Resize/Scroll listeners. 89 windowListeners(); 90 91 // Sticky Header 92 stickyHdr(); 93 94 // Toggle Mobile Nav Trigger Tabbable 95 $ww <= $mmw ? $mnt.attr("tabindex", 0) : $mnt.attr("tabindex", -1); 96 97 // Toggle Header Dropdowns 98 tglHdrDropdowns(); 99 100 // Add YouTube lazy-load listeners 101 youtubeListeners(); 102 103 // Back to Top Button 104 toTop(); 105 106 // Resource Error Listeners 107 resourceErrorLstnrs(); 108 109 // Initialize generic toggle link listeners 110 addToggleLinkListeners(); 111 112 // Add Lead Capture Listeners 113 leadListeners(); 114 115 // Default Contact Us lead capture overlay trigger 116 contactTriggers(); 117 118 // Add Kaptchu Img Listeners 119 kRefreshListeners(); 120 121 // Cookie Notification Listener 122 cookiesOkListener(); 123 124 // Product - Add To Cart 125 cartAdds(); 126 127 // Setup global ajax error handling. 128 ajaxErrors(); 129 130 // Lazy load non-critical CSS files when document is ready. To add CSS files to load, see head_tags.php meta tags 131 lazyLoad(); 132 133 // Register Service Worker 134 srvcWrkr(); 135 136 // Print Button Listeners 137 printBtnLstnrs(); 138 139 $('.filter-happenings').on("change", function (){ 140 window.open($(this).val(), '_self'); 141 }); 142 143 // ARSHRM - ADS 144 145 if($('.ad-cost').length){ 146 $('#ad_term').on('change', function () { 147 var placement_id = $('#placement_id').val(); 148 var ad_term = $(this).val(); 149 150 $('span.ad-term').html(ad_term); 151 152 // update the pricing 153 var ajaxData = {}; 154 ajaxData.action = "get_ad_cost"; 155 ajaxData.placement_id = placement_id; 156 ajaxData.ad_term = ad_term; 157 var ajaxUrl = "ajax_ads.php"; 158 159 $.ajax({ 160 url: ajaxUrl, 161 type: "POST", 162 data: ajaxData 163 }).done(function (response) { 164 // Update the ad cost text 165 $('.ad-cost').html(response); 166 }); 167 168 }); 169 170 } 171 172 //Instantiate the datepickers 173 if($('#ad_placement_form').length){ 174 175 console.log("datepickers"); 176 177 var start_date = $('#ad_start_date').val(); 178 179 console.log(start_date); 180 $('#ad_start_date').datepicker({ 181 duration: "fast", 182 showOn: "both", 183 buttonImage: "images/admin_icons/calendar_icon.png", 184 buttonImageOnly: true, 185 constrainInput: false, 186 dateFormat: 'yy/mm/dd' 187 }); 188 189 $('#ad_start_date').datepicker("setDate",start_date); 190 } 191 192 // when we change the PLACEMENT, we need to refresh the SIZE TEXT to reflect the size of the selected placement, 193 //AND we need to refresh the start/end dates to be the soonest they can be 194 $('#placement_id').on('change', function () { 195 196 var placement_id = $(this).val(); 197 console.log(placement_id); 198 199 // Update the size text for the image, so we know how big the ad needs to be :) 200 var this_label = $('#placement_id option:selected').text(); 201 202 if (this_label.indexOf("banner") >= 1) { 203 $('.size-text').html("Banner ads must be a 728px x 90px jpg"); 204 205 $('span.ad-size').html('banner'); 206 207 } else { 208 $('.size-text').html("Small ads must be a 310px x 142px jpg"); 209 210 $('span.ad-size').html('small'); 211 212 } 213 214 if($('.ad-cost').length){ 215 // update the pricing output 216 217 ad_term = $('#ad_term').val(); 218 219 var ajaxData = {}; 220 ajaxData.action = "get_ad_cost"; 221 ajaxData.placement_id = placement_id; 222 ajaxData.ad_term = ad_term; 223 var ajaxUrl = "ajax_ads.php"; 224 225 $.ajax({ 226 url: ajaxUrl, 227 type: "POST", 228 data: ajaxData 229 }).done(function (response) { 230 // Update the ad cost text 231 $('.ad-cost').html(response); 232 }); 233 } 234 235 // Get the best start/end dates. 236 var ajaxData = {}; 237 ajaxData.action = "get_start_date"; 238 ajaxData.placement_id = placement_id; 239 var ajaxUrl = "ajax_ads.php"; 240 241 $.ajax({ 242 url: ajaxUrl, 243 type: "POST", 244 data: ajaxData 245 }).done(function (response) { 246 // Remove the row from the current Manage Users list, if deleti
246on successful. 247 $('#ad_start_date').val(response); 248 var ad_start_date = response; 249 250 251 }); 252 253 }); 254 255}); 256 257 258/** 259 * Add Default Google Analytics Event Listeners 260 */ 261function addGtagEventListeners() { 262 263 if (gua_on || ga4_on) { 264 265 /** 266 * Initialize Automatic Gel GA Click Event Listeners. 267 * To cause a click on a specific element to automatically send an event to Analytics, first add the class "ga-click" to the element. 268 * 269 * GTAG/Analytics Events 270 * To trigger one or more GTAG.js (Analytics, AdWords, etc.) events when the element is clicked, add the following data attributes to the element: 271 * 272 * Universal Analytics Events: data-ua-name (corresponds to the event_action), data-ua-params (must include event_category, event_label, value, and non_interaction) 273 * GTAG/GA4 Events: data-ga4-name, data-ga4-params, data-ga4-route (optional) 274 * Not currently set up to handle AdWords tracking, but can be. 275 * 276 * The event params value should contain a list of parameter name/value pairs in the following format: 277 * data-ga4-params="name::value||name2::value2||name3::value3" (define as many recommended and/or custom parameters as required, but no more than necessary) 278 * 279 * See GA Event Tracking Guidelines for more info. 280 */ 281 $('.ga-click').on('click', function() { 282 if (gua_on) sendGtagEvent($(this).attr('data-ua-name'), parseGelGtagEventParams($(this).attr('data-ua-params')), gua_id); 283 if (ga4_on) sendGtagEvent($(this).attr('data-ga4-name'), parseGelGtagEventParams($(this).attr('data-ga4-params')), ga4_id); 284 }); 285 286 // Initialize additional default event listeners. 287 $('.bread a').on('click', function() { 288 if (gua_on) sendGtagEvent('Clicks', {"event_category": "Breadcrumbs", "event_label": $(this).text(), "value": 0, "non_interaction": true}, gua_id); 289 if (ga4_on) sendGtagEvent("click_item", {"item_title": $(this).text(), "item_url": $(this).attr('href'), "click_item_list": "Breadcrumb", "position_on_page": "Top right below header"}, ga4_id); 290 }); 291 $('.back-to-top').on('click', function () { 292 if (gua_on) sendGtagEvent('Clicks', {"event_category": "Page Interactions", "event_label": "Back to Top Link", "value": 0, "non_interaction": true}, gua_id); 293 if (ga4_on) sendGtagEvent("scroll_to_top", {"item_title": "Scroll-to-Top"}, ga4_id); 294 }); 295 296 } 297} 298 299/** 300 * Send a Google GTAG Analytics Event 301 * 302 * @param n string Name - The name of the event. 303 * @param p obj Params - A JSON object (not a JSON string) containing the parameters to send with the event. 304 * @param r string Route - Optional. Also known as "send_to". A string containing a property ID or a routing groups reference that defines the property or group(s) of properties to which the event should be sent/recorded. Leave empty or omit to send to the GA4_MEASUREMENT_ID. See https://developers.google.com/gtagjs/devguide/routing for more info. 305 */ 306function sendGtagEvent(n, p, r) { 307 if (n !== "" && p !== "" && n !== "undefined" && p !== "undefined") { 308 if (r !== "" && typeof r !== "undefined") p.send_to = r; 309 gtag('event', n, p); 310 } 311} 312 313/** 314 * Convert a Gel-formatted string list of Gtag Event Parameters into a JSON object. 315 * Expected format 316 * 317 * @param params string The list of parameters. Expected format: "name::value||name2::value2||name3::value3" etc. Any number of parameters may be defined. 318 * @returns {{}} JSON object containing the parameters, which can be sent to Analytics, AdWords Tracking, etc. 319 */ 320function parseGelGtagEventParams(params) { 321 var po = {}; 322 var ps = params.split("||"); 323 for (var i = 0; i < ps.length; i++) { 324 var pa = ps[i].split("::"); 325 po[pa[0]] = pa[1]; 326 } 327 return po; 328} 329 330/** 331 * Mitigate IE/Edge list-item=none render bug on list items hidden when page is initially loaded. 332 * Removes and replaces hidden lists forcing browser to re-render them. 333 * 2018-03-18 - This is a known issue that may be resolved at any time, at which time this fix and the function can be removed from Gel.
334 * 2019-02-05 - Still unresolved. 335 * 2020-04-14 - They made a bandaid. Keeping code to cover users who don't update on a regular basis. 336 */ 337function edgeHiddenListsFix() { 338 if (document.documentMode || /Edge/.test(navigator.userAgent)) { 339 $('ul:hidden').each(function () { 340 $(this).replaceWith($(this)); 341 }); 342 343 // Reinstate navigation listeners on tertiary items 344 addNavListeners('.menu-level-2'); 345 } 346} 347 348/** 349 * Add Window Resize/Scroll Listeners 350 * @usage Add code to be executed or re-executed when the window is resized or scrolled. 351 */ 352function windowListeners() { 353 354 // Window Resize Listener 355 $(window).resize(function() { 356 $ww = $(window).width(); 357 358 stickyHdr(); 359 360 // Toggle Mobile Nav Trigger Tabbable 361 $ww <= $mmw ? $mnt.attr("tabindex", 0) : $mnt.attr("tabindex", -1); 362 }); 363 364 // Window Scroll Listener 365 $(window).scroll(function() { 366 stickyHdr(); 367 toTop(); 368 }); 369} 370 371/** 372 * Set the fixed header listeners. 373 */ 374function stickyHdr() { 375 $(this).scrollTop() > 0 && ftr.attr('data-sticky-hdr') === "1" && $ww > parseFloat(ftr.attr('data-sticky-min')) ? hdr.addClass("header-fixed") : hdr.removeClass("header-fixed"); 376} 377 378/** 379 * Show/Hide the Scroll to Top button and add listener to buttton. 380 */ 381function toTop() { 382 $(this).scrollTop() > 500 && ftr.attr('data-to-top') ? stt.addClass('back-to-top-show') : stt.removeClass('back-to-top-show'); 383 stt.off('keydown click').on('keydown click', function(e) { 384 if (testClickEnterSpace(e)) { 385 e.stopPropagation(); 386 $('html, body').animate({scrollTop : 0},800); 387 return false; 388 } 389 }); 390} 391 392/** 393 * Add the Main Navigation Menu Dropdown Hover Listeners 394 * 395 * @param scope Optional. CSS selector to set the scope in which to apply/reapply the listeners. 396 */ 397function addNavListeners(scope) { 398 scope = (typeof scope !== 'undefined') ? scope+" " : ''; 399 400 if ($mmw > 0) { 401 var ww = $(window).width(); 402 $(window).resize(function() { 403 ww = $(window).width(); 404 }); 405 $(scope+"li").hoverIntent({ 406 over: function() { 407 // Disable sliding effect if window width is less than "STOP_MENU_ANIMATION_WIDTH" 408 if (ww > $mmw) { 409 $(">ul", this).stop().slideDown(500); 410 } else { 411 $(">ul", this).show(); 412 } 413 }, 414 out: function(){ 415 // Disable sliding effect if window width is less than "STOP_MENU_ANIMATION_WIDTH" 416 if (ww > $mmw) { 417 $(">ul", this).stop().slideUp(400); 418 } else { 419 $(">ul", this).hide(); 420 } 421 } 422 }); 423 } 424} 425 426/** 427 * Toggle Header Options Dropdowns (things like login, account dropdowns, etc. â not primary nav) 428 */ 429function tglHdrDropdowns() { 430 431 $('.hdr-opt-togl').on('keydown click', function(e) { 432 if (testClickEnterSpace(e)) { 433 e.stopPropagation(); 434 e.preventDefault(); 435 436 var $t = $(this), $hdd = $('.hdr-has-drpdwn'), $ctnr = $t.closest('.hdr-has-drpdwn'), $m = $ctnr.children('.hdr-drpdwn'), a = dfFx, o = {direction:'up'}, s = dfFxDur; 437 438 // Close any other open header dropdowns first. 439 $hdd.not($ctnr).children('.hdr-drpdwn').slideUp(s); 440 441 // Open the overlay. 442 $m.toggle(a, o, s); 443 444 // Add overlay handlers. 445 overlayHandler($m, $t, a, o, s, ""); 446 447 // Toggle ARIA button states 448 toggleAriaBtnStates($t); 449 } 450 }); 451} 452 453/** 454 * Display a temporary overlay notification, which will disappear automatically after a set period of time. 455 * 456 * @param text string Text string to be displayed in notification. 457 * @param time int Time in milliseconds that the notification should be displayed before going away. 458 * 459 * @return empty 460 */ 461function tempOverlayMessage(text, time) { 462 // Create overlay container, if not already existing. 463 if ($('#temp_overlay_message').length < 1) { 464 hdr.after('<div id="temp_overlay_message"></div>'); 465 } 466 // Now insert the new notification 467 var tomcls = "success"; 468 var tomicn = "<svg viewBox='0 0 100 100'><use xlink:href='#success' href='#success'></use></svg>"; 469 if (!errorTest(text)) { 470 tomcls = "error"; 471 tomicn ="<svg viewBox='0 0 100 100'><use xlink:href='#error' href='#error'></use></svg>"; 472 } 473 tomct++; 474 var tomid = 'overnote-'+tomct; 475 $('<div id="' + tomid + '" class="overlay-notification ' + tomcls +'">
475' + tomicn + ' ' + text +'</div>').hide().prependTo('#temp_overlay_message').slideDown(show_time, show_easing, function() { 476 setTimeout(function() { 477 $('#'+tomid).slideUp(hide_time, hide_easing, function(){ 478 $('#'+tomid).remove(); 479 }); 480 }, time); 481 }); 482} 483 484/** 485 * Hidin' dem darn emale addresses 486 * @returns {boolean} 487 */ 488function obfuscateEmails() { 489 490 var $eat = hdr.attr('data-eat'), $eau = hdr.attr('data-eau'), $eav = ftr.attr('data-eav'), $eaw = ftr.attr('data-eaw'); 491 if ((typeof $eat === typeof undefined || $eat === false) || (typeof $eau === typeof undefined || $eau === false) || (typeof $eav === typeof undefined || $eav === false) || (typeof $eaw === typeof undefined || $eaw === false)) return false; 492 var deat = $eat.split(','), deau = $eau.split(','), deav = $eav.split(','), deaw = $eaw.split(','); 493 494 for (var i = 0; i < deat.length; i++) { 495 496 var ft = 'eat'+i+'@eau'+i+'eav'+i+'eaw'+i, a = deat[i]+'@'+deau[i]+deav[i]+deaw[i], es = $('a[href="mailto:'+ft+'"]'); 497 498 if (es.length > 0) { 499 es.attr('href', 'mailto:'+a); 500 $('a[href="mailto:'+a+'"]').replaceText(ft, a); 501 } else { 502 $('body *').replaceText(ft, '<a href="mailto:'+a+'" title="Send a message to '+a+'" class="ga-click" data-ua-name="Clicks" data-ua-params="event_category::Email Links||event_label::'+a+'||value::0||nonint::false" data-ga4-name="generate_lead" data-ga4-params="value::0||method::Email Link - '+a+'||position_on_page::unknown">'+a+'</a>'); 503 } 504 } 505 return true; 506} 507 508/** 509 * Method to Find/Replace text -- Required by ObfuscateEmails() above. 510 * 511 * @param search 512 * @param replace 513 * @param text_only 514 * @returns {*} 515 */ 516$.fn.replaceText = function(search, replace, text_only) { 517 return this.each(function() { 518 var node = this.firstChild, 519 val, 520 new_val, 521 remove = []; 522 if ( node ) { 523 do { 524 if ( node.nodeType === 3 ) { 525 val = node.nodeValue; 526 new_val = val.replace( search, replace ); 527 if ( new_val !== val ) { 528 if ( !text_only && /</.test( new_val ) ) { 529 $(node).before( new_val ); 530 remove.push( node ); 531 } else { 532 node.nodeValue = new_val; 533 } 534 } 535 } 536 } while (node = node.nextSibling); 537 } 538 remove.length && $(remove).remove(); 539 }); 540}; 541 542/** 543 * Add generic toggle link listeners to elements with class .toggle-link. Version 2 544 * Ignores html at the end of the link text. 545 * 546 * Listener will slide toggle the display (using a slide effect) of a targeted container using one or more css selectors defined in a "data-container-id" attribute added to the clickable element. 547 * Example: <span class="toggle-link" data-container-id="#advanced_search">More Search Options</span> 548 * Clicking this span element will toggle the display of a container with id of "advanced_search". 549 * Example: <span class="toggle-link" data-container-id="#advanced_search .search-buttons">More Search Options</span> 550 * Clicking this span element will toggle the display of a container with id of "advanced_search" and all containers with class of "search-buttons". 551 * 552 * Optionally, listener will also switch the text of the link if two additional data attributes exist: "data-toggle-text-one" and "data-toggle-text-two". 553 * The first text attribute value should match the initial link text, and the second should be the alternative link text. 554 * Example: <span class="toggle-link" data-container-id="advanced_search" data-toggle-text-one="More Search Options" data-toggle-text-two="Fewer Search Options">More Search Options</span> 555 * In addition to the display toggle, this link will also toggle the link text from "More Search Options" to "Fewer Search Options", and vice versa. 556 * NOTE: Will not work if the text-one text does not initially match the text inside the toggle-link element, though ignores html at the end of the link text. 557 * 558 * @param s string Scope Optional. Add a selector to indicate the scope within which new listeners should be applied. For example, to only apply new listeners in a modal box, provide the id selector of the modal box, i.e. "#modal-box". 559 */ 560function addToggleLinkListeners(s) { 561 // Param format is an optional value. If we don't have a param value, we set a default value which is currently an empty string '' 562 s = (typeof s !== 'undefined') ? s+" " : ''; 563 564 // Toggle Open/Closed State of the target. 565 $(s+".toggle-link").off().each(function() { 566 var c = $(this).attr("data-container-id"); 567 $(this).off('keydown click').on('keydown click', function(e) { 568 if (testClickEnterSpace(e)) { 569 // Test for pounds and periods for backwards compatibility. 570 !c.match(/[(#|.)]/ig) ? c = "#"+c : null; 571 572 $(c).slideToggle(); 573 574 // Check if the link text should be replaced 575 if ($(this).attr("data-toggle-text-one") && $(this).attr("data-toggle-text-two")) { 576 var t1 = $(this).attr("data-toggle-text-one"); // t1 = text one 577 var t2 = $(this).attr("data-toggle-text-two"); // t2 = text two 578 var lp = $(this).html().match(/([^<]+)(<[^]+)?/i); // Sometimes html is appended, such as toggle svg icons. Capture the link text and html separately. lp = link parts 579 if (lp[1] === t1) { 580 if (lp[2] !== undefined) { 581 t2 = t2+lp[2]; 582 } 583 $(this).html(t2); 584 } else if (lp[1] === t2) { 585 if (lp[2] !== undefined) { 586 t1 = t1+lp[2]; 587 } 588 $(this).html(t1); 589 } 590 } 591 592 // Toggle ARIA button states 593 toggleAriaBtnStates($(this)); 594 } 595 }); 596 597 }); 598 599 // Toggle Fieldset Class 600 $(s+".fieldset-toggle").on('keydown click', function(e) { 601 if (testClickEnterSpace(e)) { 602 $(this).toggleClass("fieldset-open fieldset-closed"); 603 } 604 }); 605 606 // Toggle Mobile Nav Open/Closed Classes 607 $(s+".nav-trigger").on('keydown click', function(e) { 608 if (testClickEnterSpace(e)) { 609 $(this).toggleClass("nav-open nav-closed"); 610 } 611 }); 612
613 //ADDED FOR ARSHRM - STATE CHAP TOGGLES 614 $(s+".chapter-toggle-link").off().each(function() { 615 var c = $(this).attr("data-container-id"); 616 $(this).off('keydown click').on('keydown click', function(e) { 617 if (testClickEnterSpace(e)) { 618 // Test for pounds and periods for backwards compatibility. 619 !c.match(/[(#|.)]/ig) ? c = "#"+c : null; 620 621 $('.chapter-active').removeClass('chapter-active'); 622 $(c).addClass('chapter-active'); 623 $(this).addClass('chapter-active'); 624 625 // Toggle ARIA button states 626 toggleAriaBtnStates($(this)); 627 } 628 }); 629 630 }); 631 632 $(s+".chapter-map-link").on('click', function (){ 633 var b = $(this).attr("data-button-id"); 634 var c = $('#'+b).attr("data-container-id"); 635 636 $('.chapter-active').removeClass('chapter-active'); 637 $('#'+c).addClass('chapter-active'); 638 $('#'+b).addClass('chapter-active'); 639 640 // Toggle ARIA button states 641 toggleAriaBtnStates($(this)); 642 643 }); 644 645} 646 647/** 648 * Add Listeners to Select menus that send the user to a specific results filter URL when an option is selected 649 * 650 * @param slctr string The element/id/class selector used to target the html select element on which to place the listener. 651 */ 652function urlSelectFilterListeners(slctr) { 653 $(slctr).on('change', function (){ 654 console.log("urlSelectFilterListeners"); 655 window.open($(this).val(), '_self'); 656 }); 657} 658 659/** 660 * Image pre-loader. 661 * 662 * @param imgs array 663 */ 664function preload(imgs) { 665 $(imgs).each(function(){ 666 $('<img/>')[0].src = this; 667 }); 668} 669 670/** 671 * Setup global ajax error handling. 672 */ 673function ajaxErrors() { 674 // Setup global ajax error handling. 675 $.ajaxSetup({ 676 error: function (x, status, error) { 677 if (x.status === 403) { 678 alert("Sorry, a 403 forbidden error occurred."); 679 } else { 680 alert("An error occurred: " + status + " Error: " + error); 681 } 682 } 683 }); 684} 685 686 687/** 688 * Add YouTube Lazy Loader Listeners. 689 * 690 * @usage To lazy load a YouTube video, include a div element with a class of "youtube", data-id containing the video ID, and optional video data-title. 691 * @example <div class="youtube" data-id="Hl7QkwKo0vY" data-title="'19 Sportsmen 271BHSE 360 VR Tour"></div> 692 * 693 * @notes The function will lazy load the YouTube thumbnail on document load, then will load the YouTube embed iframe on click. 694 * There are corresponding CSS styles that must be present. 695 * Video plays now trigger a GA event when GA enabled. 696 */ 697function youtubeListeners() { 698 var id, title; 699 700 $('.youtube').each(function(){ 701 id = $(this).attr('data-id'); 702 title = $(this).attr('data-title'); 703 704 // Implement the placeholder display. 705 $( '<div/>', { 706 'class': "youtube-thumb "+id, 707 'data-id': id, 708 html: '<img src="https://i.ytimg.com/vi/'+id+'/hqdefault.jpg" alt="'+title+' Video Thumbnail"><div class="youtube-play"></div>'+(title !== '' ? '<p>'+title+'</p>' : '') 709 }).appendTo($(this)); 710 711 }).promise().done(function(){ 712 $('.youtube-thumb').click(function () { 713 id = $(this).attr('data-id'); 714 if (gua_on) sendGtagEvent('Plays', {"event_category": "Videos", "event_label": title, "value": 0, "non_interaction": false}, gua_id); 715 var iframe = $( "<iframe/>", { 716 'src': "https://www.youtube.com/embed/"+id+"?autoplay=1", 717 'frameborder': "0", 718 'allowfullscreen': "1", 719 'allow': "autoplay; encrypted-media" 720 }); 721 $('.'+id).replaceWith(iframe); 722 }); 723 }); 724} 725 726/** 727 * Add Gel Cookie Usage Notification acceptance listener. 728 */ 729function cookiesOkListener() { 730 $("#accept_cookies").submit( function(e) { 731 e.preventDefault(); 732 733 var $ac = $("#accept_cookies"); 734 735 $.ajax({ 736 url: $ac.attr('data-ajax-script'), 737 type: 'POST', 738 data: $ac.serialize(), 739 }).done(function(response){ 740 if (response === 'Success!') { 741 // We did it! hide the notice. 742 $('.cookies').slideUp(); 743 } 744 }); 745 }); 746} 747 748/** 749 * Add Captcha Image Refresh Listeners 750 */ 751function kRefreshListeners() { 752 $('.invisacap_refresh').off('keydown click').on('keydown click', function(e) { 753 if (testClickEnterSpace(e)) { 754 e.stopPropagation(); 755 756 showWorkingLoaderSplash(); 757 758 // Set new image source 759 $($(this).attr("data-image-selector")).attr("src", $(this).attr("data-url")+invis_counter); 760 761 setTimeout(function() { 762 hideWorkingLoaderSplash(); 763 }, 200); 764 765 invis_counter++; 766 } 767 }); 768} 769 770/** 771 * Add Main Contact Overlay lead trigger handlers. 772 */ 773function contactTriggers() { 774 $('.contact-us-trigger').on('keydown click', function(e) { 775 if (testClickEnterSpace(e)) { 776 e.stopPropagation(); 777 var $t = $(this), $cu = $('#contact-us'), a = dfFx, o = {direction:'up'}, s = dfFxDur; 778 779 // Open the overlay. 780 $cu.toggle(a, o, s); 781 782 // Add overlay handlers. 783 overlayHandler($cu, $t, a, o, s, ""); 784 } 785 }); 786} 787 788/** 789 * Initialize Generic Lead Capture Form Listeners 790 * See the lead capture script for details of usage. 791 * To be included, a form must have a class of "lead-form" and have a parent container the working loader splash and replacement response can be applied to. 792 * Note: GelOverlayPopups have an applicable parent container by default. 793 */ 794function leadListeners() { 795 796 // Add form submission listeners. 797 $(".lead-form").submit( function(e) { 798 e.preventDefault(); 799 var form = $(this); 800 var parent = form.parent(); 801 802 $.ajax({ 803 url: '/'+subdir_path+'ajax-lead-capture.php', 804 type: 'POST', 805 dataType: 'json', 806 data: form.serialize(), 807 beforeSend: function() { 808 parent.hasClass('overlay-container') ? showWorkingLoaderSplash() : showWorkingLoaderSplash(parent); 809 }, 810 success: function(data) { 811 hideWorkingLoaderSplash(); 812 813 var ehr = $('.error-highlight'); 814 var ile = $('.inline-error'); 815 816 switch (data.status) { 817 818 case 'valid': 819 820 // Send the corresponding analytics event. 821 gua_on ? sendGtagEvent(form.attr("data-ua-name"), parseGelGtagEventParam
821s(form.attr("data-ua-params")), gua_id) : null; 822 ga4_on ? sendGtagEvent(form.attr("data-ga4-name"), parseGelGtagEventParams(form.attr("data-ga4-params")), ga4_id) : null; 823 824 // Replace form with response output. 825 animateReplace(form, data.output).done(function() { 826 // Reset the overlay handlers. Uses the olGlbls global variable array to reset the handlers using the same settings that the overlay was initialized with. 827 overlayHandler(olGlbls["m"], olGlbls["n"], olGlbls["a"], olGlbls["o"], olGlbls["s"], olGlbls["c"]); 828 }); 829 830 break; 831 832 case 'invalid': 833 834 // Remove previous errors first. 835 ehr.removeClass('error-highlight'); 836 ile.html(''); 837 838 // Loop through response object and display errors. 839 for (var i = 0; i < data.errors.length; i++) { 840 841 // Highlight inputs with errors 842 var name = data.errors[i].name; 843 form.find('#'+name+'-input').addClass('error-highlight'); 844 if (i === 0) { 845 // This is the first error 846 form.find('#'+name+'-input').focus(); 847 } 848 849 // Add inline error messages. 850 var messages = data.errors[i].messages; 851 for (var m = 0; m < messages.length; m++) { 852 var this_error = messages[m]; 853 form.find('#'+name+'-error').append('<li>'+this_error+'</li>'); 854 } 855 } 856 857 break; 858 859 case 'error': 860 861 // Remove previous errors first. 862 ehr.removeClass('error-highlight'); 863 ile.html(''); 864 865 tempOverlayMessage(data.error, 4500); 866 867 break; 868 869 } 870 }, 871 error: function() { 872 hideWorkingLoaderSplash(); 873 tempOverlayMessage('Error: Something odd went wrong attempting submitting your request. Please try again or try our contact page.', 4500); 874 } 875 }); 876 }); 877} 878 879/** 880 * Set handlers when a Gel Overlay Popup has been launched. â 2021.3 881 * 882 * @param {Object} m Modal. jQuery Object - the modal container to be closed. Listener will be scoped to this object. 883 * @param {Object} n Next Focus. jQuery Object - the element that should receive focus after the modal is closed. Should be the element that triggered the modal dialog to begin with. 884 * @param {String} a Animation Toggle Effect. Optional. jQuery Toggle effect to use when closing the modal, i.e., 'slide', 'blind', or 'fade'. 885 * @param {Object} o Options (toggle effect). Optional. jQuery UI Toggle effect options as array object, i.e. {direction:"right"} (for slide or blind) 886 * @param {Number} s Speed (toggle effect). Optional. Speed for the Toggle effect, i.e., 300. Defaults to global hide_time value. 887 * @param {String} c Classes (show/hide). Optional. Show/Hide Toggle Classes. String containing two class names separated by a space to toggle show/hide the modal/dialog. 888 * 889 * @usage 890 * Call this function AFTER opening a modal dialog (usually a GelOverlayPopup). 891 * When any of the close events are triggered, the modal will be closed using either a jQuery animation or via CSS show/hide toggle Classes (c). 892 * To specify a toggle effect, options and speed, set 'a' to the name of a jQuery UI Toggle effect, such as "slide", "blind" or "fade", and optionally set c to the speed. If no speed is specified, the value of the default global variable hide_time will be used 893 * If neither a nor c are specified, the modal will be closed using 'slideUp' and the hide_time speed. 894 */ 895function overlayHandler(m, n, a, o, s, c) { 896 897 // Add the persistent variables to the Overlay Globals variable so we don't have to keep passing them around and we have access to them anywhere. 898 olGlbls = {"m":m, "n":n, "a":a, "o":o, "s":s, "c":c}; 899 900 // Trap focus withing the dialog and get the first focusable element back. 901 trapFocus(m); 902 903 // Set Close Event listeners on the overlay-outer, overlay-close, dismiss overlay elements. 904 $('.overlay-outer, .overlay-close, .dismiss-overlay').off('keydown click').on('keydown click', function(e) { 905 if (testClickEnterSpace(e)) { 906 e.stopPropagation(); 907 e.preventDefault(); 908 closeOverlay(m); 909 } 910 }); 911 912 // Set Close Event Triggers and Handling on the Escape key. 913 escModal(m); 914 915 // Keep clicks the overlay content from closing the overlay or creating a double-click situation on the close icon. 916 $('.overlay-inner').off("click").click(function(e) { 917 e.stopPropagation(); 918 }); 919 m.off("click").click(function(e) { 920 e.stopPropagation(); 921 }); 922 923} 924 925/** 926 * Trap tab focus within a container, such as a dialog modal. â 2021.3 927 * 928 * @param {Object} m Container in which to trap focus. 929 * @returns {Object} f The first tabbable element in the container. 930 */ 931function trapFocus(m) { 932 933 // Set First Focus inside modal and Trap Focus within the Dialog. 934 var r = /^(Tab|9)$/; 935 936 // Find the first and last focusable elements. 937 var tb = [...m.find('a, button, input, textarea, select, details, [tabindex]:not
937([tabindex="-1"])')].filter(el => !el.hasAttribute('disabled') && !el.getAttribute("aria-hidden") && !(el.type === "a" && !el.hasAttribute("href"))); 938 var f = $(tb[0]), l = $(tb[tb.length-1]); 939 940 // Get the first and last focusable elements, and trap focus within the dialog. 941 f.focus(); 942 f.off('keydown').keydown(function(e) { 943 if (e.shiftKey && keydownTest(e, r)) { 944 e.stopPropagation(); 945 l.focus(); 946 return false; 947 } 948 }); 949 l.off('keydown').keydown(function(e) { 950 if (!e.shiftKey && keydownTest(e, r)) { 951 e.stopPropagation(); 952 f.focus(); 953 return false; 954 } 955 }); 956 957 return f; 958} 959 960/** 961 * Add Escape Modal Listener â 2021.3 962 * 963 * @param m Obj Modal. jQuery Object - the modal/overlay/popup container to be closed or hidden. Listener will be scoped to this object. 964 */ 965function escModal(m) { 966 m.off('keydown').on('keydown', function(e) { 967 if (keydownTest(e, /^(Escape|Esc|27)$/)) { 968 e.preventDefault(); 969 closeOverlay(m); 970 } 971 }); 972} 973 974/** 975 * Close Modal, Overlay or Popup â 2021.3 976 * 977 * @param m Obj Modal. jQuery Object - the modal/overlay/popup container to be closed. Listener will be scoped to this object. 978 * 979 * @usage Also relies on the olGlbls global variable, into which should have been stored the next focus element and optional close css or animation effect settings for the modal. 980 */ 981function closeOverlay(m) { 982 983 var a = olGlbls["a"], o = olGlbls["o"], s = olGlbls["s"], c = olGlbls["c"], n = olGlbls["n"]; 984 985 // First, remove the close listeners. 986 $('.overlay-outer, .overlay-close, .dismiss-overlay').off('keyboard click'); 987 988 // Close the overlay modal. 989 if (typeof c !== 'undefined' && c !== '') { 990 m.toggleClass(c); 991 } else if (typeof a !== 'undefined' && a !== '') { 992 m.toggle(a, undefinedEmpty(o) ? {} : o, undefinedEmpty(s) ? hide_time : s); 993 } else { 994 m['slideUp'](hide_time); 995 } 996 997 // Toggle Aria States on the original trigger. 998 toggleAriaBtnStates(n); 999 1000 // Set focus back in the document to n. 1001 olGlbls["n"].focus(); 1002 olGlbls = []; 1003 1004 // Remove click handler from body 1005 $('body').off('keydown click'); 1006} 1007 1008/** 1009 * Test a parameter is undefined or empty. 1010 * @param p parameter 1011 * @returns {boolean} 1012 */ 1013function undefinedEmpty(p) { 1014 return typeof p === 'undefined' || p === ""; 1015} 1016 1017/** 1018 * Add Gel Resource download error listeners. 1019 */ 1020function resourceErrorLstnrs() { 1021 $('a.resource-download-error-link').each(function() { 1022 1023 var anchor = $(this); 1024 var $rdebox = $('#resource_download_error_box'); 1025 1026 $(anchor).click(function() { 1027 var cntnt = $(anchor).attr('data-error-box-content'); 1028 if (cntnt !== '') { 1029 $rdebox.html(cntnt); 1030 } 1031 $rdebox.dialog({ 1032 draggable: false, 1033 modal: true, 1034 resizable: false, 1035 width: "auto", 1036 buttons: { 1037 Ok: function() { 1038 $(this).dialog("close"); 1039 } 1040 } 1041 }); 1042 return false; 1043 }); 1044 }); 1045} 1046 1047/** 1048 * Add to Cart Listeners 1049 */ 1050function cartAdds() { 1051 $atc_form.on('click', function () { 1052 1053 var redirect = $('#product_add_to_cart_redirect').val(); 1054 var qty = $('#quantity').val(); 1055 1056 // Ajax request to update order and retrieve receipt for display. 1057 var data = {}; 1058 data.product_id = $('#product_id').val(); 1059 data.quantity = qty; 1060 data.product_add_to_cart_redirect = redirect; 1061 data.product_max_quantity = $('#product_max_quantity').val(); 1062 data.action = 'add-to-cart'; 1063 1064 // process with AJAX 1065 $.ajax({ 1066 url: '/'+subdir_path+'ajax-finance-functions.php', 1067 type: 'POST', 1068 data: data, 1069 beforeSend: function() { 1070 showWorkingLoaderSplash($atc_response, ''); 1071 } 1072 }).done(function(data){ 1073 1074 if (!errorTest(data)) { 1075 1076 $atc_response.fadeOut('fast', function() { 1077 $atc_response.html(data); 1078 $atc_response.fadeIn('slow'); 1079 1080 setTimeout(function(){ 1081 $atc_response.html(''); 1082 $atc_response.fadeOut('slow'); 1083 }, 5000); 1084 }); 1085 1086 } else { 1087 1088 switch (redirect) { 1089 1090 case 'No': 1091 1092 // Update the cart icon display. 1093 var $cartnum = $('.cart-number'); 1094 var old_count = $cartnum.html(); 1095 old_count = (old_count === '' || old_count === 'undefined' ? 0 : old_count); 1096 var new_count = parseInt(old_count)+parseInt(qty); 1097 $cartnum.html(new_count); 1098 $hcart.removeClass('display-none'); 1099 1100 // Show success message. 1101 $atc_response.fadeOut('fast', function() { 1102 $atc_response.html(data); 1103 $atc_response.fadeIn('slow'); 1104 1105 setTimeout(function(){ 1106 $atc_response.html(''); 1107 $atc_response.fadeOut('slow'); 1108 }, 5000); 1109 }); 1110 1111 break; 1112 1113 case 'Cart': 1114 case 'Checkout': 1115 1116 window.location.href = data; 1117 1118 break; 1119 } 1120 } 1121 }); 1122 }); 1123} 1124 1125/** 1126 * Lazy load non-critical CSS files when document is ready. To add CSS files to load, see head_tags.php meta tags 1127 */ 1128function lazyLoad() { 1129 // Function to lazy load non-critical CSS files. To add CSS files to load, see head_tags.php meta tags. 1130 var lzld = function() { 1131 1132 // Get the meta tag containing a list of CSS files to lazy load. 1133 var css = document.getElementById('loadcss').getAttribute('data-css'); 1134 1135 if (typeof css !== 'undefined' && css !== '') { 1136 var fls = css.split(','); 1137 for (var i = 0; i < fls.length; i++) { 1138 1139 // Get the file name. 1140 var fl = fls[i]; 1141 1142 // Create the link element and appended it to the head. 1143 var l = document.createElement('link'); 1144 l.rel = 'stylesheet'; 1145 l.href = document.getElementById('loadcss').getAttribute('data-css-base') + fl; 1146 l.media = 'all'; 1147 l.type = 'text/css'; 1148 1149 // Insert the css link element into the head. 1150 document.getElementsByTagName('head')[0].appendChild(l); 1151 } 1152 } 1153 }; 1154 1155 // This version would use the links defined in noscript, which ultimately we should do. 1156 /*var lzld = function() { 1157 var dcss = document.getElementById('deferred-css'); 1158 var rcss = document.createElement('div'); 1159 rcss.innerHTML = dcss.textContent; 1160 document.body.appendChild(rcss); 1161 dcss.parentElement.removeChild(dcss); 1162 };*/ 1163 1164 // This is part of the lazy loading of non-critical CSS files above. I still don't understand why we use the animation frame method. 1165 var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; 1166 if (raf) raf(function() { window.setTimeout(lzld, 0); }); 1167 else window.addEventListener('load', lzld); 1168} 1169 1170/** 1171 * Register Service Worker 1172 */ 1173function srvcWrkr() { 1174 // Check browser compatibility before registering. 1175 if ("serviceWorker" in navigator) { 1176 // Register the service worker 1177 navigator.serviceWorker 1178 .register("/"+subdir_path+"serviceworker.js", { 1179 1180 }) 1181 .then(function (reg) { 1182 console.log("Service worker has been registered for scope: " + reg.scope); 1183 }); 1184 } 1185} 1186 1187/** 1188 * Unregister the Service Worker 1189 * Comment this code out to reset the service worker in your browser, then comment it back out when done. 1190 */ 1191/*function killSrvcWrkr() { 1192 navigator.serviceWorker.getRegistrations().then( 1193 function(registrations) { 1194 for (let registration of registrations) { 1195 console.log("Die!"); 1196 registration.unregister(); 1197 } 1198 }); 1199}*/ 1200 1201 1202/** 1203 * Add a Working Progress Loader Overlay 1204 * 1205 * @param t str Target element selector. Selector name for the element the loader overlay should be applied to. Default is 'body'. 1206 * @param i str Image - Reference to image to be used for the loader. Defaults to 'images/gel-ajax-loader-big-gray-spinner-2.gif'. 1207 */ 1208function showWorkingLoaderSplash(t,i) { 1209 hideWorkingLoaderSplash(); 1210 var fix = ' class="loader-absolute"'; 1211 if (t === '' || typeof t === 'undefined') { 1212 t = 'body'; 1213 fix = ''; 1214 } 1215 if (i === '' || typeof i === 'undefined') { 1216 i = 'images/gel-ajax-loader-big-gray-spinner-2.gif'; 1217 } 1218 $(t).addClass('loader-o').prepend('<div id="working_loader"'+fix+'><img src="'+i+'" alt="Working Loader Image"></div>').fadeIn(50); 1219} 1220 1221/** 1222 * Remove the current Working Loader Overlay. 1223 */ 1224function hideWorkingLoaderSplash() { 1225 $('#working_loader').remove(); 1226 $('*').removeClass('loader-o'); 1227} 1228 1229/** 1230 * Remove a DOM element by highlighting, fading and sliding it first. 1231 * 1232 * @param e Element to be removed. 1233 * 1234 * @returns promise 1235 */ 1236function highlightAndRemove(e) { 1237 return $.Deferred(function(hr) { 1238 e.animate({ 1239 backgroundColor: highlight_color 1240 }, 500) 1241 .animate({ 1242 height: "toggle", 1243 opacity: "toggle" 1244 }, { 1245 duration: 1200, 1246 complete: function () { 1247 $(this).remove(); 1248 hr.resolve(); 1249 } 1250 }) 1251 }).promise(); 1252} 1253 1254/** 1255 * Replace a DOM element with data via fade and slide animation. 1256 * 1257 * @param e Obj Element - jQuery Object - The Element to Replace 1258 * @param r The replacement 1259 * 1260 * @returns promise 1261 */ 1262function animateReplace(e, r) { 1263 return $.Deferred(function(ar) { 1264 e.animate({ 1265 height: "toggle", 1266 opacity: "toggle" 1267 }, { 1268 duration: 1200, 1269 complete: function() { 1270 $(this).replaceWithPush(r, true).animate({ 1271 height: "toggle", 1272 opacity: "toggle" 1273 }, {
1274 duration: 1200, 1275 complete: function () { 1276 ar.resolve(); 1277 } 1278 }); 1279 } 1280 }); 1281 }).promise(); 1282} 1283 1284/** 1285 * jQuery replaceWith, plus return the replacement element. 1286 * @param a 1287 * @param hide 1288 * @returns {*|jQuery|HTMLElement} 1289 */ 1290$.fn.replaceWithPush = function(a, hide) { 1291 var $a = $(a); 1292 if (hide) { 1293 $a.hide(); 1294 } 1295 this.replaceWith($a); 1296 return $a; 1297}; 1298 1299/** 1300 * Test a response (or any string) for standard error output. 1301 * 1302 * @param rsps str 1303 * @returns {boolean} True if NO error found, otherwise false. 1304 */ 1305function errorTest(rsps) { 1306 return (rsps.indexOf("Error:") === -1 && rsps.indexOf("Oops!") === -1 && rsps.indexOf('class="error"') === -1); 1307} 1308 1309/* Accessibility */ 1310 1311/** 1312 * Test a keydown event for specific key or code. 1313 * 1314 * @param e event The event. 1315 * @param r regex A JS regex string, i.e., /^( |Enter|13|32)$/ The string may contain any combination of key or code values. 1316 * @returns {boolean} 1317 */ 1318function keydownTest(e, r) { 1319 if (e.type === "keydown") { 1320 var key = e.key || e.which; 1321 return r.test(key); 1322 } else { 1323 return false; 1324 } 1325} 1326 1327/** 1328 * Test whether an event was a click or Space or Enter keydown. 1329 * @param e event 1330 * @returns {boolean} 1331 */ 1332function testClickEnterSpace(e) { 1333 if (e.type === "click") { 1334 return true; 1335 } else { 1336 return keydownTest(e, /^( |Enter|13|32)$/); 1337 } 1338} 1339 1340/** 1341 * Toggle an object's aria-pressed and aria-expanded boolean value and return its aria-controls id. 1342 * 1343 * @param e The jQuery object with aria-pressed and/or aria-expanded attribute. 1344 * @returns {*} Returns 1345 */ 1346function toggleAriaBtnStates(e) { 1347 if (e.attr("aria-pressed")) { 1348 e.attr("aria-pressed", e.attr("aria-pressed") === "false") 1349 } 1350 if (e.attr("aria-expanded")) { 1351 e.attr("aria-expanded", e.attr("aria-expanded") === "false") 1352 } 1353 return e.attr("aria-controls"); 1354} 1355 1356/** 1357 * Add Print Button Listeners 1358 */ 1359function printBtnLstnrs() { 1360 $('.print-btn').on('keydown click', function(e) { 1361 if (testClickEnterSpace(e)) { 1362 e.stopPropagation(); 1363 window.print(); 1364 } 1365 }); 1366}
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.