PageSourceSearch

https://talbothotel.co.uk/wp-content/plugins/mr-blocks/assets/animation/js/frontend.js?ver=0.1.5

js talbothotel.co.uk collected 2026-10-09 00:15:08 UTC 13,340 bytes, 585 lines download raw bytes

1/**
2 * MR Blocks Animation Frontend Engine
3 * Phase 4: Complete animation system with Intersection Observer, hover handlers, and performance optimization
4 */
5
6class MRAnimationEngine {
7	constructor() {
8		this.observers = new Map();
9		this.animatedElements = new Set();
10		this.isInitialized = false;
11		this.isReducedMotion = false;
12		this.isMobile = false;
13		this.debugMode = false;
14
15		// Performance tracking
16		this.performanceMetrics = {
17			elementsProcessed: 0,
18			animationsTriggered: 0,
19			startTime: performance.now(),
20		};
21
22		// Bind methods
23		this.handleIntersection = this.handleIntersection.bind(this);
24		this.handleResize = this.debounce(this.handleResize.bind(this), 250);
25		this.cleanup = this.cleanup.bind(this);
26	}
27
28	/**
29	 * Initialize the animation engine
30	 */
31	init() {
32		if (this.isInitialized) return;
33
34		this.log("Initializing MR Blocks Animation Engine");
35
36		// Check for reduced motion preference
37		this.checkReducedMotion();
38
39		// Detect mobile devices
40		this.detectMobile();
41
42		// Find and process animated elements
43		this.findAnimatedElements();
44
45		// Setup intersection observers
46		this.setupIntersectionObservers();
47
48		// Setup hover handlers
49		this.setupHoverHandlers();
50
51		// Setup event listeners
52		this.setupEventListeners();
53
54		this.isInitialized = true;
55		this.log(
56			`Initialized with ${this.animatedElements.size} animated elements`,
57		);
58	}
59
60	/**
61	 * Check if user prefers reduced motion
62	 */
63	checkReducedMotion() {
64		this.isReducedMotion =
65			window.matchMedia &&
66			window.matchMedia("(prefers-reduced-motion: reduce)").matches;
67
68		if (this.isReducedMotion) {
69			this.log("Reduced motion detected - animations will be minimized");
70			document.documentElement.classList.add("mr-anim-reduced-motion");
71		}
72	}
73
74	/**
75	 * Detect mobile devices
76	 */
77	detectMobile() {
78		this.isMobile =
79			/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
80				navigator.userAgent,
81			) || window.innerWidth <= 768;
82
83		if (this.isMobile) {
84			this.log("Mobile device detected");
85			document.documentElement.classList.add("mr-anim-mobile");
86		}
87	}
88
89	/**
90	 * Find all animated elements on the page
91	 */
92	findAnimatedElements() {
93		const elements = document.querySelectorAll(".animated");
94
95		elements.forEach((element) => {
96			if (!this.animatedElements.has(element)) {
97				this.processAnimatedElement(element);
98				this.animatedElements.add(element);
99				this.performanceMetrics.elementsProcessed++;
100			}
101		});
102	}
103
104	/**
105	 * Process individual animated element
106	 */
107	processAnimatedElement(element) {
108		// Parse animation settings from classes
109		const settings = this.parseAnimationSettings(element);
110
111		// Store settings on element
112		element._mrAnimSettings = settings;
113
114		// Hide element initially if not hover animation
115		if (!settings.isHover && !this.isReducedMotion) {
116			element.classList.add("mr-anim-hidden");
117		}
118
119		this.log("Processed element:", settings);
120	}
121
122	/**
123	 * Parse animation settings from element classes
124	 */
125	parseAnimationSettings(element) {
126		const classes = element.className;
127
128		// Extract animation type
129		const animationTypes = [
130			"bounce",
131			"flash",
132			"pulse",
133			"rubberBand",
134			"shakeX",
135			"shakeY",
136			"headShake",
137			"swing",
138			"tada",
139			"wobble",
140			"jello",
141			"heartBeat",
142			"fadeIn",
143			"fadeInDown",
144			"fadeInLeft",
145			"fadeInRight",
146			"fadeInUp",
147			"fadeInDownBig",
148			"fadeInLeftBig",
149			"fadeInRightBig",
150			"fadeInUpBig",
151			"bounceIn",
152			"bounceInDown",
153			"bounceInLeft",
154			"bounceInRight",
155			"bounceInUp",
156			"slideInDown",
157			"slideInLeft",
158			"slideInRight",
159			"slideInUp",
160			"zoomIn",
161			"zoomInDown",
162			"zoomInLeft",
163			"zoomInRight",
164			"zoomInUp",
165			"backInDown",
166			"backInLeft",
167			"backInRight",
168			"backInUp",
169			"rotateIn",
170			"rotateInDownLeft",
171			"rotateInDownRight",
172			"rotateInUpLeft",
173			"rotateInUpRight",
174			"fadeOut",
175			"fadeOutDown",
176			"fadeOutLeft",
177			"fadeOutRight",
178			"fadeOutUp",
179			"bounceOut",
180			"bounceOutDown",
181			"bounceOutLeft",
182			"bounceOutRight",
183			"bounceOutUp",
184			"slideOutDown",
185			"slideOutLeft",
186			"slideOutRight",
187			"slideOutUp",
188			"zoomOut",
189			"zoomOutDown",
190			"zoomOutLeft",
191			"zoomOutRight",
192			"zoomOutUp",
193			"flip",
194			"flipInX",
195			"flipInY",
196			"flipOutX",
197			"flipOutY",
198			"lightSpeedInRight",
199			"lightSpeedInLeft",
200			"lightSpeedOutRight",
201			"lightSpeedOutLeft",
202			"hinge",
203			"jackInTheBox",
204			"rollIn",
205			"rollOut",
206		];
207
208		const animationType = animationTypes.find((type) => classes.includes(type));
209
210		// Extract speed
211		let speed = "default";
212		if (classes.includes("slower")) speed = "slower";
213		else if (classes.includes("slow")) speed = "slow";
214		else if (classes.includes("faster")) speed = "faster";
215		else if (classes.includes("fast")) speed = "fast";
216
217		// Extract delay
218		const delayMatch = classes.match(
219			/mr-anim-value-delay-(\d+(?:\.\d+)?)(ms|s)/,
220		);
221		const delay = delayMatch ? delayMatch[0] : null;
222
223		// Check if hover animation
224		const isHover = classes.includes("mr-anim-hover");
225
226		// Extract trigger offset
227		const offsetMatch = classes.match(/mr-anim-offset-(\d+)%/);
228		const triggerOffset = offsetMatch ? parseInt(offsetMatch[1]) / 100 : 0.1; // Default 10%
229
230		// Check for repeat
231		const isRepeat = classes.includes("repeat") || classes.includes("infinite");
232
233		return {
234			animationType,
235			speed,
236			delay,
237			isHover,
238			triggerOffset,
239			isRepeat,
240			element,
241			hasTriggered: false,
242		};
243	}
244
245	/**
246	 * Setup Intersection Observers for scroll-triggered animations
247	 */
248	setupIntersectionObservers() {
249		if (!window.IntersectionObserver) {
250			this.log(
251				"IntersectionObserver not supported, falling back to immediate trigger",
252			);
253			this.fallbackToImmediateTrigger();
254			return;
255		}
256
257		// Group elements by trigger offset for efficiency
258		const offsetGroups = new Map();
259
260		this.animatedElements.forEach((element) => {
261			const settings = element._mrAnimSettings;
262			if (!settings.isHover) {
263				const offset = settings.triggerOffset;
264				if (!offsetGroups.has(offset)) {
265					offsetGroups.set(offset, []);
266				}
267				offsetGroups.get(offset).push(element);
268			}
269		});
270
271		// Create observer for each offset group
272		offsetGroups.forEach((elements, offset) => {
273			const observer = new IntersectionObserver(this.handleIntersection, {
274				threshold: offset,
275				rootMargin: "0px",
276			});
277
278			elements.forEach((element) => {
279				observer.observe(element);
280			});
281
282			this.observers.set(offset, observer);
283			this.log(
284				`Created observer for offset ${offset * 100}% with ${
285					elements.length
286				} elements`,
287			);
288		});
289	}
290
291	/**
292	 * Handle intersection observer callbacks
293	 */
294	handleIntersection(entries) {
295		entries.forEach((entry) => {
296			if (entry.isIntersecting) {
297				const element = entry.target;
298				const settings = element._mrAnimSettings;
299
300				if (!settings.hasTriggered || settings.isRepeat) {
301					this.triggerAnimation(element, settings);
302					settings.hasTriggered = true;
303					this.performanceMetrics.animationsTriggered++;
304
305					// Stop observing if not repeating
306					if (!settings.isRepeat) {
307						entry.target._observer?.unobserve(entry.target);
308					}
309				}
310			}
311		});
312	}
313
314	/**
315	 * Setup hover handlers for hover animations
316	 */
317	setupHoverHandlers() {
318		this.animatedElements.forEach((element) => {
319			const settings = element._mrAnimSettings;
320
321			if (settings.isHover) {
322				// Remove hidden class for hover animations
323				element.classList.remove("mr-anim-hidden");
324
325				if (this.isMobile) {
326					// On mobile, use touch events
327					this.setupMobileHoverAlternative(element, settings);
328				} else {
329					// Desktop hover events
330					element.addEventListener("mouseenter", () => {
331						this.triggerAnimation(element, settings);
332					});
333
334					element.addEventListener("mouseleave", () => {
335						this.resetAnimation(element, settings);
336					});
337				}
338			}
339		});
340	}
341
342	/**
343	 * Setup mobile hover alternatives
344	 */
345	setupMobileHoverAlternative(element, settings) {
346		// On mobile, trigger animation on first touch/tap
347		const handleTouch = () => {
348			this.triggerAnimation(element, settings);
349			element.removeEventListener("touchstart", handleTouch);
350		};
351
352		element.addEventListener("touchstart", handleTouch, { passive: true });
353	}
354
355	/**
356	 * Trigger animation on element
357	 */
358	triggerAnimation(element, settings) {
359		if (this.isReducedMotion) {
360			// For reduced motion, just show the element
361			element.classList.add("mr-anim-ready");
362			element.classList.remove("mr-anim-hidden");
363			return;
364		}
365
366		// Remove hidden class and add ready class
367		element.classList.remove("mr-anim-hidden");
368		element.classList.add("mr-anim-ready");
369
370		// Apply delay if specified
371		if (settings.delay) {
372			element.style.animationDelay = settings.delay.replace(
373				"mr-anim-value-delay-",
374				"",
375			);
376		}
377
378		// Apply speed class
379		if (settings.speed !== "default") {
380			element.classList.add(settings.speed);
381		}
382
383		this.log(`Triggered ${settings.animationType} animation on element`);
384
385		// Dispatch custom event
386		element.dispatchEvent(
387			new CustomEvent("mrAnimationTriggered", {
388				detail: { settings },
389			}),
390		);
391	}
392
393	/**
394	 * Reset animation (for hover animations)
395	 */
396	resetAnimation(element, settings) {
397		if (settings.isHover && !this.isReducedMotion) {
398			// Force reflow to restart animation
399			element.style.animation = "none";
400			element.offsetHeight; // Trigger reflow
401			element.style.animation = null;
402		}
403	}
404
405	/**
406	 * Fallback for browsers without IntersectionObserver
407	 */
408	fallbackToImmediateTrigger() {
409		this.animatedElements.forEach((element) => {
410			const settings = element._mrAnimSettings;
411			if (!settings.isHover) {
412				setTimeout(() => {
413					this.triggerAnimation(element, settings);
414				}, 100);
415			}
416		});
417	}
418
419	/**
420	 * Setup event listeners
421	 */
422	setupEventListeners() {
423		// Handle window resize
424		window.addEventListener("resize", this.handleResize);
425
426		// Handle page visibility changes
427		document.addEventListener("visibilitychange", () => {
428			if (document.hidden) {
429				this.pauseAnimations();
430			} else {
431				this.resumeAnimations();
432			}
433		});
434
435		// Handle page unload
436		window.addEventListener("beforeunload", this.cleanup);
437
438		// Listen for reduced motion changes
439		if (window.matchMedia) {
440			const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
441			mediaQuery.addEventListener("change", () => {
442				this.checkReducedMotion();
443				this.refreshAnimations();
444			});
445		}
446	}
447
448	/**
449	 * Handle window resize
450	 */
451	handleResize() {
452		const wasMobile = this.isMobile;
453		this.detectMobile();
454
455		if (wasMobile !== this.isMobile) {
456			this.log("Device type changed, refreshing animations");
457			this.refreshAnimations();
458		}
459	}
460
461	/**
462	 * Pause animations (when page is hidden)
463	 */
464	pauseAnimations() {
465		document.documentElement.classList.add("mr-anim-paused");
466	}
467
468	/**
469	 * Resume animations (when page becomes visible)
470	 */
471	resumeAnimations() {
472		document.documentElement.classList.remove("mr-anim-paused");
473	}
474
475	/**
476	 * Refresh animations (useful for dynamic content)
477	 */
478	refreshAnimations() {
479		this.log("Refreshing animations");
480
481		// Find new animated elements
482		this.findAnimatedElements();
483
484		// Cleanup old observers
485		this.observers.forEach((observer) => observer.disconnect());
486		this.observers.clear();
487
488		// Re-setup observers
489		this.setupIntersectionObservers();
490		this.setupHoverHandlers();
491	}
492
493	/**
494	 * Cleanup resources
495	 */
496	cleanup() {
497		this.log("Cleaning up animation engine");
498
499		this.observers.forEach((observer) => observer.disconnect());
500		this.observers.clear();
501		this.animatedElements.clear();
502
503		window.removeEventListener("resize", this.handleResize);
504		window.removeEventListener("beforeunload", this.cleanup);
505	}
506
507	/**
508	 * Get performance metrics
509	 */
510	getPerformanceMetrics() {
511		const currentTime = performance.now();
512		return {
513			...this.performanceMetrics,
514			runtime: currentTime - this.performanceMetrics.startTime,
515			isInitialized: this.isInitialized,
516			elementsActive: this.animatedElements.size,
517			observersActive: this.observers.size,
518		};
519	}
520
521	/**
522	 * Utility: Debounce function
523	 */
524	debounce(func, wait) {
525		let timeout;
526		return function executedFunction(...args) {
527			const later = () => {
528				clearTimeout(timeout);
529				func(...args);
530			};
531			clearTimeout(timeout);
532			timeout = setTimeout(later, wait);
533		};
534	}
535
536	/**
537	 * Logging utility
538	 */
539	log(...args) {
540		if (this.debugMode || window.mrAnimationDebug) {
541			console.log("[MR Animation]", ...args);
542		}
543	}
544}
545
546// Initialize the animation engine
547let animationEngine;
548
549document.addEventListener("DOMContentLoaded", function () {
550	animationEngine = new MRAnimationEngine();
551	animationEngine.init();
552
553	// Expose for debugging
554	window.mrAnimation = animationEngine;
555});
556
557// Handle dynamic content loading (for AJAX/SPA)
558if (window.MutationObserver) {
559	const observer = new MutationObserver(function (mutations) {
560		let shouldRefresh = false;
561
562		mutations.forEach(function (mutation) {
563			mutation.addedNodes.forEach(function (node) {
564				if (
565					node.nodeType === 1 && // Element node
566					(node.classList?.contains("animated") ||
567						node.querySelector?.(".animated"))
568				) {
569					shouldRefresh = true;
570				}
571			});
572		});
573
574		if (shouldRefresh && animationEngine) {
575			animationEngine.refreshAnimations();
576		}
577	});
578
579	observer.observe(document.body, {
580		childList: true,
581		subtree: true,
582	});
583}
584
585console.log("MR Blocks Animation Frontend Engine loaded");

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.