PageSourceSearch

https://www.townofredoaknc.com/js/custom.230531181955.js

js townofredoaknc.com collected 2026-10-04 13:23:31 UTC 16,887 bytes, 628 lines download raw bytes

1/* JS for preset "Menu V2" */
2(function() {
3	$(function() {
4		$('.menu-wrapper').each(function() {
5			initMenu($(this))
6		});
7	});
8
9	// Make :active pseudo classes work on iOS
10	document.addEventListener("touchstart", function() {}, false);
11
12	var initMenu = function($menuWrapper) {
13		var $body = $('body');
14		var $menu = $('.ed-menu', $menuWrapper);
15		var $menuLinks = $('a', $menu);
16		var $menuTrigger = $('.menu-trigger', $menuWrapper);
17		var $banner = $('.banner').first();
18
19		var menuWrapperHeight = $menuWrapper.outerHeight();
20		var bannerHeight = $banner.length ? $banner.outerHeight() : 0;
21		var smoothScrollOffset = 20;
22		
23		toggleClassOnClick($body.add($menu), $menuTrigger, null, 'open open-menu'); // Keep open on $menu for backward compatibility
24		activateSmoothScroll($menuLinks.add($('.scroll a')), smoothScrollOffset);
25		addClassOnVisibleLinkTargets($menuLinks, 'active', 2 / 3);
26		handleSticky($menuWrapper, 'sticky', $banner);
27	};
28
29	/**
30	 * Observe element's height changes and reload the initMenu() function
31	 *
32	 * @param {HTMLElement} elm Element to observe
33	 * @param {function} callback to call when elmement's height changed
34	 */
35	var observeHeightChange = function(elm, callback) {
36		if (!('ResizeObserver' in window) || elm == null) return;
37
38		var ro = new ResizeObserver(callback);
39		ro.observe(elm);
40	}
41
42	/**
43	 * Toggles class on a target when a trigger is clicked
44	 * 
45	 * @param {jQuery} $target The target to apply the CSS class to
46	 * @param {jQuery} $trigger The Trigger
47	 * @param {jQuery} $closeTrigger Optional close trigger
48	 * @param {string} cssClass CSS Class to toggle on the target
49	 */
50	var toggleClassOnClick = function($target, $trigger, $closeTrigger, cssClass) {
51
52		// Reset in case class "open" was saved accidentally
53		$target.removeClass(cssClass);
54		$trigger.removeClass(cssClass);
55
56		// Click on trigger toggles class "open"
57		$trigger.off('.toggle').on('click.toggle', function() {
58			$(this).toggleClass(cssClass);
59			$target.toggleClass(cssClass);
60		});
61
62		// Close target when link inside is clicked
63		$target.find('a').click(function() {
64			$target.removeClass(cssClass);
65			$trigger.removeClass(cssClass);
66		});
67
68		if (!$closeTrigger || !$closeTrigger.length) {
69			return;
70		}
71
72		$closeTrigger.click(function() {
73			$target.removeClass(cssClass);
74			$trigger.removeClass(cssClass);
75		});
76	};
77
78	/**
79	 * Smooth scroll to link targets
80	 * 
81	 * @param {jQuery} $scrollLinks The links
82	 * @param {jQuery} scrollOffset Offset to subtract from the scroll target position (e.g. for fixed positioned elements like a menu)
83	 */
84	var activateSmoothScroll = function($scrollLinks, scrollOffset) {
85		if (typeof scrollOffset === 'undefined') {
86			scrollOffset = 0;
87		}
88
89		var determineTarget = function($trigger, hash) {
90			if (hash == '#!next') {
91				return $trigger.closest('.ed-element').next();
92			}
93
94			return $(hash);
95		}
96
97		$scrollLinks.click(function(e) {
98			var $target = determineTarget($(this), this.hash);
99			if (!$target.length) return;
100			e.preventDefault();
101
102			viewport.scrollTo($target, 'top', 500, 0);
103
104		});
105	};
106
107	/**
108	 * We are using the fill property on an element to pass user's choices from CSS to JavaScript
109	 * 
110	 * @param {jQuery} $element
111	 */
112	var getStickyMode = function($element) {
113		var fillValue = getComputedStyle($element[0]).fill;
114
115		return fillValue === 'rgb(255, 0, 0)' ?
116			'sticky_banner' :
117			fillValue === 'rgb(0, 255, 0)' ?
118			'sticky_menu' :
119			fillValue === 'rgb(0, 0, 255)' ?
120			'sticky_instant' :
121			fillValue === 'rgb(255, 255, 255)' ?
122			'sticky_reverse' :
123			'sticky_none';
124	};
125
126	/**
127	 * Adds a class to an element when not currently visible
128	 * 
129	 * @param {jQuery} $element The element to handle stickyness for
130	 * @param {string} cssClass The actual CSS class to be applied to the element when it's above a certain scroll position
131	 * @param {jQuery} $banner A banner to reference the scroll position to
132	 */
133	var handleSticky = function($element, cssClass, $banner) {
134		var triggerPos = 0,
135			offset = 0;
136		var menuWrapperHeight = $element.outerHeight();
137		var mode;
138		var prevScroll = 0;
139		$element.removeClass(cssClass);
140		var updateOffset = function() {
141			mode = getStickyMode($element);
142			if (!$element.hasClass(cssClass)) {
143				offset = $element.offset().top;
144				menuWrapperHeight = $element.outerHeight();
145			}
146			if (mode === 'sticky_banner' && !$banner.length) {
147				mode = 'sticky_menu';
148			}
149			if (mode === 'sticky_banner') {
150				triggerPos = $banner.offset().top + ($banner.length ? $banner.outerHeight() : $element.outerHeight());
151			}
152			if (mode === 'sticky_menu' || mode === 'sticky_reverse') {
153				triggerPos = offset + $element.outerHeight();
154			}
155			if (mode === 'sticky_instant') {
156				triggerPos = offset + 1;
157			}
158		}
159		viewport.observe('resize', updateOffset);
160		viewport.observe('animation.end', updateOffset);
161		observeHeightChange($element[0], updateOffset);
162		updateOffset();
163
164		var toggleSpacer = function(toggle) {
165			document.body.style.setProperty('--spacer-height', toggle ? menuWrapperHeight + 'px' : '');
166		};
167
168		var handleScroll = function() {
169			if (!$element.length || mode === 'sticky_none') return;
170
171			var isReverse = mode === 'sticky_reverse',
172				curScroll = viewport.getScrollTop();
173
174			if (triggerPos <= curScroll && (!isReverse || prevScroll > curScroll)) {
175				$element.addClass(cssClass);
176				toggleSpacer(true);
177			} else {
178				$element.removeClass(cssClass);
179				toggleSpacer(false);
180			}
181
182			prevScroll = curScroll;
183		};
184		viewport.observe('scroll', handleScroll);
185		handleScroll();
186	};
187
188	/**
189	 * Adds a class to links whose target is currently inside the viewport
190	 * 
191	 * @param {jQuery} $links Link(s) to be observed
192	 * @param {string} cssClass CSS Class to be applied
193	 * @param {float} sectionViewportRatio Ratio by which the target should be within the viewport
194	 */
195	var addClassOnVisibleLinkTargets = function($links, cssClass, sectionViewportRatio) {
196		if (typeof sectionViewportRatio === 'undefined') {
197			sectionViewportRatio = 1 / 2;
198		}
199
200		var menuTargets = [];
201		var activeLink = $links.filter('.active:not(.wv-link-elm)').eq(0);
202
203		var links = $links.filter(function() {
204			var $target = $(this.hash);
205			if (!$target.length) {
206				return false;
207			}
208
209			// Cache offset position to improve performance (update on resize)		
210			var updateOffset = function() {
211				$target.data('offset', $target.offset().top);
212			};
213
214			viewport.observe('resize', updateOffset);
215			viewport.observe('animation.end', updateOffset);
216			updateOffset();
217
218			menuTargets.push($target);
219			return true;
220		});
221
222		// No hash links found, so don't handle it at all
223		if (!links.length) {
224			return;
225		}
226
227		var checkVisibility = function() {
228			$links.removeClass('active');
229
230			// Check section position reversely
231			for (var i = menuTargets.length - 1; i >= 0; i--) {
232				var desiredScrollPosition = menuTargets[i].data('offset') - viewport.getHeight() * (1 - sectionViewportRatio);
233				if (viewport.getScrollTop() >= desiredScrollPosition && menuTargets[i][0].offsetParent !== null) {
234					links.eq(i).addClass(cssClass);
235					return;
236				}
237			}
238
239			// Fallback to originally active item
240			activeLink.addClass(cssClass);
241		};
242
243		viewport.observe('scroll', checkVisibility);
244		checkVisibility();
245	};
246})();
247/* End JS for preset "Menu V2" */
248
249/* JS for preset "Counter" */
250$(function() {
251	EasingFunctions = {
252		// no easing, no acceleration
253		linear: function(t) {
254			return t
255		},
256		// accelerating from zero velocity
257		easeInQuad: function(t) {
258			return t * t
259		},
260		// decelerating to zero velocity
261		easeOutQuad: function(t) {
262			return t * (2 - t)
263		},
264		// acceleration until halfway, then deceleration
265		easeInOutQuad: function(t) {
266			return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
267		},
268		// accelerating from zero velocity 
269		easeInCubic: function(t) {
270			return t * t * t
271		},
272		// decelerating to zero velocity 
273		easeOutCubic: function(t) {
274			return (--t) * t * t + 1
275		},
276		// acceleration until halfway, then deceleration 
277		easeInOutCubic: function(t) {
278			return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
279		},
280		// accelerating from zero velocity 
281		easeInQuart: function(t) {
282			return t * t * t * t
283		},
284		// decelerating to zero velocity 
285		easeOutQuart: function(t) {
286			return 1 - (--t) * t * t * t
287		},
288		// acceleration until halfway, then deceleration
289		easeInOutQuart: function(t) {
290			return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
291		},
292		// accelerating from zero velocity
293		easeInQuint: function(t) {
294			return t * t * t * t * t
295		},
296		// decelerating to zero velocity
297		easeOutQuint: function(t) {
298			return 1 + (--t) * t * t * t * t
299		},
300		// acceleration until halfway, then deceleration 
301		easeInOutQuint: function(t) {
302			return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
303		}
304	}
305
306	$('[data-count]').each(function() {
307		var duration = 2000;
308		var interval = Math.floor(duration / 60);
309		var steps = Math.ceil(duration / interval);
310		var $target = $(this);
311		var offset = $target.offset().top;
312		var max = $target.data('count');
313		$target.html('0');
314		var started = false;
315
316		var startCounter = function() {
317		    if (started) return;
318		    
319		    started = true;
320			var i = 0,
321				handle;
322			handle = window.setInterval(function() {
323				$target.html(Math.round(max * EasingFunctions.easeOutCubic(i / steps)));
324				i++;
325				if (i == steps) {
326					$target.html(max);
327					window.clearInterval(handle);
328				}
329			}, interval);
330		};
331
332		viewport.observe('scroll', function() {
333			if ((viewport.getScrollTop() + viewport.getHeight() / 1) > offset) {
334				startCounter();
335			}
336		});
337	});
338});
339/* End JS for preset "Counter" */
340
341/* JS for preset "Content Slider" */
342$(function() {
343	if (document.body.classList.contains('edit') || document.body.classList.contains('preview')) {
344		return;
345	}
346
347	viewport.promise('api.slick.ready', function() {
348		viewport.jQuery('.content-slider-wrap > .inner').slick({
349			autoplay: true,
350			arrows: true,
351			prevArrow: '<button type="button" data-role="none" class="slick-prev" tabindex="0" role="button"></button>',
352			nextArrow: '<button type="button" data-role="none" class="slick-next" tabindex="0" role="button"></button>',
353			speed: 300,
354			autoplaySpeed: 3000,
355			dots: false,
356			adaptiveHeight: true,
357			fade: false
358		});
359	}).requireSlick();
360});
361
362/* End JS for preset "Content Slider" */
363
364/* JS for preset "Back to top button V3" */
365(function() {
366    var initBackToTop = function() {
367        var $button = $('.back-to-top-button-icon');
368        
369    	clickToTop($button);
370    	
371    	// Show back to top only below the fold
372    	viewport.observe('scroll', function() {
373    		if (viewport.getScrollTop() > (viewport.getHeight() / 3)) {
374    			$button.addClass('show');
375    		} else {
376    			$button.removeClass('show');
377    		}
378    	});
379    };
380    
381    var clickToTop = function($trigger) {
382    	$trigger.removeClass('show');
383    
384    	$trigger.click(function(e) {
385    		e.preventDefault();
386    		viewport.scrollTo(0, 'top', 500, 0);
387    	});
388    };
389    
390    $(initBackToTop);
391})();
392/* End JS for preset "Back to top button V3" */
393
394/* JS for preset "Typewriter" */
395$(function() {
396	$('.tw-instance').each(function() {
397		typeWriter($(this))
398	});
399});
400
401const typeWriter = function($twInstance) {
402	const $target = $twInstance;
403	const $tw_output = $('.tw-output', $target);
404	let index = 0,
405		isAdding = true
406	let text = [];
407	let current = 0;
408	let animationLoop = 0;
409	let writing_speed,
410		deleting_speed,
411		cursor_ticking,
412		pause,
413		pause_hover,
414		loop,
415		trigger,
416		isHovering;
417
418	// Wait for load
419	const ready = function(callback) {
420		var fn = function() {
421			callback();
422		};
423
424		if (window.readyState !== 'loading') {
425			fn();
426			return;
427		}
428		document.addEventListener('DOMContentLoaded', fn);
429	};
430
431	// Save the texts
432	$('.text-container .ed-element.ed-text', $target).text(function(index, currentcontent) {
433		text[index] = currentcontent;
434	});
435
436	const typeWriterAnim = function() {
437		const timeoutId = setTimeout(() => {
438			if (String(timeoutId) !== $twInstance[0].dataset.timeoutId) return;
439		
440			if (isHovering && pause_hover) {
441			    typeWriterAnim();
442			    return;
443			}
444			
445			//Stop animation when editing
446			if ($('.text-container .ed-element.ed-text', $target).hasClass('wv-editing')) {
447			    typeWriterAnim();
448			    return;
449			}
450			
451			if (text[current] !== undefined) {
452				$tw_output[0].innerHTML = text[current].substring(0, index);
453			}
454
455			if (isAdding) {
456				// adding text
457				if ($tw_output[0].innerHTML.length == 0) {
458					$tw_output.addClass("showAnim");
459					$tw_output.removeClass("hideCursor");
460					setTimeout(() => {
461						index++;
462					}, pause);
463				} else {
464					if (index > text[current].length) {
465						// no more text to add
466						isAdding = false;
467
468						// pause before starting deletion
469						$tw_output.addClass("showAnim");
470						setTimeout(function() {
471							typeWriterAnim();
472						}, pause);
473						return;
474					} else {
475						// increment index by 1
476						index++;
477						$tw_output.removeClass("showAnim");
478					}
479				}
480			} else {
481				// removing text
482				if (index === 0) {
483					// blink again
484					$tw_output.addClass("showAnim");
485					// no more text to remove
486					isAdding = true;
487
488					//Re-parsing settings (edit mode only)
489					if (document.body.classList.contains('edit')) {
490						handleSettingValues();
491						reParseTexts();
492					}
493
494					// handle loop
495					if (loop) {
496						current = (current + 1) % text.length;
497					} else {
498						current = (current + 1);
499						// keep looping on edit mode (just for show)
500						if (document.body.classList.contains('edit') && current === text.length) {
501							current = 0;
502						}
503					}
504				} else {
505					//keep last text visible (not on edit mode)
506					if (!document.body.classList.contains('edit')) {
507						if (loop === false && current === text.length - 1) {
508							$tw_output.removeClass("showAnim");
509							$tw_output.addClass("hideCursor");
510							return;
511						}
512					}
513					// decrement index by 1
514					index--;
515					$tw_output.removeClass("showAnim");
516				}
517			}
518			typeWriterAnim();
519		}, isAdding ? writing_speed : deleting_speed);
520		$twInstance[0].dataset.timeoutId = timeoutId;
521	}
522
523	// Parse and handle the values from preset
524	const handleSettingValues = function() {
525		const values = window.getComputedStyle($target[0], ':before').content.slice(1, -1).split('-');
526		let ms_pattern = /[ms]\D/;
527		let parsedValues = [];
528
529		values.forEach(value => {
530			if (value === 'false' || value === "true") {
531				parsedValues.push(JSON.parse(value));
532			} else if (ms_pattern.test(value)) {
533				parsedValues.push(parseFloat(value.replace('ms', '')))
534			} else {
535				parsedValues.push(parseFloat(value.replace('s', '')) * 1000)
536			}
537		});
538
539		writing_speed = parsedValues[0];
540		deleting_speed = parsedValues[1];
541		cursor_ticking = parsedValues[2];
542		pause = parsedValues[3];
543		pause_hover = parsedValues[4];
544		loop = parsedValues[5];
545
546		// Handle mouse hover
547		if (pause_hover) {
548			$tw_output[0].addEventListener(('mouseenter'), function() {
549				isHovering = true;
550			});
551			$tw_output[0].addEventListener(('mouseleave'), function() {
552				isHovering = false;
553			});
554
555			// Necessary for "pause on hover" (on edit mode)
556			$tw_output.addClass("pauseHover");
557		} else {
558			$tw_output.removeClass("pauseHover");
559		}
560
561	}
562
563	const reParseTexts = function() {
564		// Refresh text
565		text = [];
566
567		$('.text-container .ed-element.ed-text', $target).each(function(index, currentcontent) {
568			text[index] = $(currentcontent).text();
569		});
570	}
571
572	handleSettingValues();
573
574	/**
575	 * Get the trigger value
576	 * We are using the fill property on an element to pass user's choices from CSS to JavaScript
577	 * 
578	 * @param {jQuery} $element
579	 */
580	const getTrigger = function($element) {
581		let triggerValue = getComputedStyle($element[0]).fill;
582
583		return triggerValue === 'rgb(0, 255, 0)' ?
584			'scroll' :
585			'load';
586	};
587
588	//Play animation on "scroll into element"
589	const animationObserver = new IntersectionObserver((entries, observer) => {
590		entries.forEach(entry => {
591			if (entry.isIntersecting) {
592				animationLoop += 1;
593				if (animationLoop <= 1) {
594					typeWriterAnim();
595				}
596			}
597		})
598	}, {
599		root: document.window,
600		rootMargin: "0px",
601	});
602
603	//Start animation 
604	ready(function() {
605
606		//Initialize values if needed
607		writing_speed = isNaN(writing_speed) ? 200 : writing_speed
608		deleting_speed = isNaN(deleting_speed) ? 100 : deleting_speed
609		cursor_ticking = isNaN(cursor_ticking) ? 200 : cursor_ticking
610		pause = isNaN(pause) ? 500 : pause
611		pause_hover === undefined ? pause_hover = false : pause_hover;
612		loop === undefined ? loop = true : loop;
613
614		trigger = getTrigger($tw_output);
615
616		if (trigger === 'scroll') {
617			if (!document.body.classList.contains('edit')) {
618				animationObserver.observe($tw_output[0]);
619			} else {
620				typeWriterAnim();
621			}
622		}
623		if (trigger === 'load') {
624			typeWriterAnim();
625		}
626	});
627};
628/* End JS for preset "Typewriter" */

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.