PageSourceSearch

https://www.arshrm.com/scriptos/gel.core.js

js arshrm.com collected 2026-10-03 22:11:13 UTC 41,714 bytes, 1,366 lines download raw bytes

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.