1/** 2 * Video Booking Form JavaScript 3 * Handles form submission, redirects to Guestline booking system, and placeholder image transitions 4 */ 5 6document.addEventListener("DOMContentLoaded", function () { 7 // Handle play button functionality 8 handleVideoPlayButton(); 9 10 // Handle placeholder image to video transition 11 handleVideoPlaceholderTransition(); 12 13 // Handle booking form functionality 14 const form = document.getElementById("booking-form"); 15 16 if (!form) { 17 console.warn("Video Booking Form: Form element not found"); 18 return; 19 } 20 21 // Initialize Flatpickr for check-in and check-out 22 const checkinInput = document.getElementById("checkin"); 23 const checkoutInput = document.getElementById("checkout"); 24 25 if (checkinInput && checkoutInput && typeof flatpickr === "function") { 26 27 // Define checkout picker first (variable declaration) 28 let checkoutPicker; 29 30 // Initialize Check-in Picker 31 const checkinPicker = flatpickr(checkinInput, { 32 altInput: true, 33 altFormat: "d/m/Y", 34 dateFormat: "Y-m-d", 35 minDate: "today", 36 disableMobile: true, 37 onChange: function (selectedDates, dateStr) { 38 // Validation: Ensure checkout picker is ready 39 if (!checkoutPicker) return; 40 41 // Set the minimum date for checkout to the day after checkin 42 if (selectedDates.length > 0) { 43 const nextDay = new Date(selectedDates[0]); 44 nextDay.setDate(nextDay.getDate() + 1); 45 checkoutPicker.set("minDate", nextDay); 46 47 // Auto-select next day for check-out 48 // Logic: If no date selected OR selected date is invalid regarding new check-in 49 if (checkoutPicker.selectedDates.length === 0 || checkoutPicker.selectedDates[0] <= selectedDates[0]) { 50 checkoutPicker.setDate(nextDay); 51 } 52 } 53 } 54 }); 55 56 // Initialize Check-out Picker 57 checkoutPicker = flatpickr(checkoutInput, { 58 altInput: true, 59 altFormat: "d/m/Y", 60 dateFormat: "Y-m-d", 61 minDate: "today", 62 disableMobile: true, 63 onOpen: function (selectedDates, dateStr, instance) { 64 // If checkin is selected but checkout minDate isn't set yet 65 if (checkinPicker && checkinPicker.selectedDates.length > 0) { 66 const nextDay = new Date(checkinPicker.selectedDates[0]); 67 nextDay.setDate(nextDay.getDate() + 1); 68 instance.set("minDate", nextDay); 69 } 70 } 71 }); 72 } 73 74 // Handle form submission 75 form.addEventListener("submit", function (e) { 76 e.preventDefault(); 77 78 // Get form data 79 const submitButton = form.querySelector('button[type="submit"]'); 80 const blockContainer = document.querySelector(".redcat-video-booking-form"); 81 82 // Get dynamic values from data attributes (like booking-buttons block) 83 const hotel = 84 submitButton?.dataset.hotel || blockContainer?.dataset.hotelCode; 85 const bookingBaseUrl = 86 blockContainer?.dataset.bookingUrl || 87 "https://booking.eu.guestline.app/RCI/availability"; 88 89 const checkin = document.getElementById("checkin")?.value; 90 const checkout = document.getElementById("checkout")?.value; 91 const guests = document.getElementById("guests")?.value; 92 93 // Validate all required fields 94 if (!hotel) { 95 alert( 96 "Hotel code is missing. Please configure the hotel code in block settings." 97 ); 98 return; 99 } 100 101 if (!checkin) { 102 alert("Please select a check-in date."); 103 document.getElementById("checkin")?.focus(); 104 return; 105 } 106 107 if (!checkout) { 108 alert("Please select a check-out date."); 109 document.getElementById("checkout")?.focus(); 110 return; 111 } 112 113 if (!guests) { 114 alert("Please select the number of guests."); 115 document.getElementById("guests")?.focus(); 116 return; 117 } 118 119 // Validate date logic 120 const checkinDate = new Date(checkin); 121 const checkoutDate = new Date(checkout); 122 123 if (checkoutDate <= checkinDate) { 124 alert("Check-out date must be after check-in date."); 125 document.getElementById("checkout")?.focus(); 126 return; 127 } 128 129 // Show loading state 130 const originalText = submitButton.innerHTML; 131 submitButton.innerHTML = "<span>Loading...</span>"; 132 submitButton.disabled = true; 133 134 try { 135 // Construct Guestline booking URL 136 const bookingParams = new URLSearchParams({ 137 hotel: hotel, 138 arrival: checkin, 139 departure: checkout, 140 adults: guests, 141 children: "0", 142 }); 143 144 const bookingUrl = `${bookingBaseUrl}?${bookingParams.toString()}`; 145 146 // Open booking page in new tab 147 window.open(bookingUrl, "_blank", "noopener,noreferrer"); 148 149 // Reset form after successful submission 150 setTimeout(() => { 151 form.reset(); 152 submitButton.innerHTML = originalText; 153 submitButton.disabled = false;
154 }, 1000); 155 } catch (error) { 156 console.error("Booking form error:", error); 157 alert( 158 error.message || 159 "An error occurred while opening the booking page. Please try again." 160 ); 161 162 // Restore button state 163 submitButton.innerHTML = originalText; 164 submitButton.disabled = false; 165 } 166 }); 167 168 // Add loading states for better UX 169 const inputs = form.querySelectorAll("input, select"); 170 inputs.forEach((input) => { 171 input.addEventListener("focus", function () { 172 this.parentElement.classList.add("focused"); 173 }); 174 175 input.addEventListener("blur", function () { 176 this.parentElement.classList.remove("focused"); 177 }); 178 }); 179}); 180 181/** 182 * Handles the transition from placeholder image to video when video loads 183 */ 184function handleVideoPlaceholderTransition() { 185 const placeholderImage = document.querySelector(".video-placeholder-image"); 186 const videoIframe = document.querySelector(".background-video-iframe"); 187 188 if (!placeholderImage || !videoIframe) { 189 return; // No placeholder image or video, nothing to transition 190 } 191 192 // Function to fade out placeholder and fade in video 193 function transitionToVideo() { 194 // Add fade-out class to placeholder 195 placeholderImage.classList.add("fade-out"); 196 197 // Add fade-in class to video and make it visible 198 videoIframe.classList.add("fade-in"); 199 200 // Remove placeholder from DOM after transition completes 201 setTimeout(() => { 202 if (placeholderImage.classList.contains("fade-out")) { 203 placeholderImage.style.display = "none"; 204 } 205 }, 800); // Match CSS transition duration 206 } 207 208 // Method 1: Listen for iframe load event 209 videoIframe.addEventListener("load", function () { 210 // Add a small delay to ensure video content starts playing 211 setTimeout(transitionToVideo, 500); 212 }); 213 214 // Method 2: Fallback timer - transition after reasonable time even if load event doesn't fire 215 setTimeout(() => { 216 if (!placeholderImage.classList.contains("fade-out")) { 217 console.log("Video placeholder: Transitioning via fallback timer"); 218 transitionToVideo(); 219 } 220 }, 3000); // 3 seconds fallback 221 222 // Method 3: Enhanced detection for YouTube/Vimeo specific events 223 if (videoIframe.src.includes("youtube.com")) { 224 handleYouTubeTransition(videoIframe, transitionToVideo); 225 } else if (videoIframe.src.includes("vimeo.com")) { 226 handleVimeoTransition(videoIframe, transitionToVideo); 227 } else if (videoIframe.src.includes("cloudflarestream.com")) { 228 handleCloudflareStreamTransition(videoIframe, transitionToVideo); 229 } 230} 231 232/** 233 * Enhanced Cloudflare Stream video load detection 234 */ 235function handleCloudflareStreamTransition(iframe, callback) { 236 // Cloudflare Stream has fast loading, similar approach to Vimeo 237 const checkCloudflareReady = () => { 238 try { 239 if (iframe.contentDocument !== null) { 240 callback(); 241 return true; 242 } 243 } catch (e) { 244 // Expected CORS error 245 } 246 return false; 247 }; 248 249 let attempts = 0; 250 const maxAttempts = 20; 251 252 const intervalId = setInterval(() => { 253 attempts++; 254 if (checkCloudflareReady() || attempts >= maxAttempts) { 255 clearInterval(intervalId); 256 if (attempts >= maxAttempts) { 257 console.log( 258 "Cloudflare Stream transition: Max attempts reached, proceeding with transition" 259 ); 260 callback(); 261 } 262 } 263 }, 250); 264} 265 266/** 267 * Enhanced YouTube video load detection 268 */ 269function handleYouTubeTransition(iframe, callback) { 270 // YouTube iframe API detection 271 const checkYouTubeReady = () => { 272 try { 273 // This will fail due to CORS, but iframe.contentDocument being accessible 274 // often indicates the iframe content has loaded 275 if (iframe.contentDocument !== null) { 276 callback(); 277 return true; 278 } 279 } catch (e) { 280 // Expected CORS error, but we can still use other indicators 281 } 282 return false; 283 }; 284 285 // Check every 250ms for up to 5 seconds 286 let attempts = 0; 287 const maxAttempts = 20; 288 289 const intervalId = setInterval(() => { 290 attempts++; 291 if (checkYouTubeReady() || attempts >= maxAttempts) { 292 clearInterval(intervalId); 293 if (attempts >= maxAttempts) { 294 console.log( 295 "YouTube transition: Max attempts reached, proceeding with transition" 296 ); 297 callback(); 298 } 299 } 300 }, 250); 301} 302 303/** 304 * Enhanced Vimeo video load detection 305 */ 306function handleVimeoTransition(iframe, callback) { 307 // Similar approach for Vimeo 308 const checkVimeoReady = () => { 309 try { 310 if (iframe.contentDocument !== null) { 311 callback(); 312 return true; 313 } 314 } catch (e) { 315 // Expected CORS error 316 } 317 return false;
318 }; 319 320 let attempts = 0; 321 const maxAttempts = 20; 322 323 const intervalId = setInterval(() => { 324 attempts++; 325 if (checkVimeoReady() || attempts >= maxAttempts) { 326 clearInterval(intervalId); 327 if (attempts >= maxAttempts) { 328 console.log( 329 "Vimeo transition: Max attempts reached, proceeding with transition" 330 ); 331 callback(); 332 } 333 } 334 }, 250); 335} 336 337/** 338 * Handles play button click to start video playback 339 */ 340function handleVideoPlayButton() { 341 const playButtonOverlay = document.querySelector(".video-play-button-overlay"); 342 const playButton = document.querySelector(".video-play-button"); 343 const videoIframe = document.querySelector(".background-video-iframe"); 344 345 if (!playButtonOverlay || !playButton || !videoIframe) { 346 return; // No play button or video, nothing to handle 347 } 348 349 // Get autoplay status from iframe data attribute 350 const autoplay = videoIframe.getAttribute("data-autoplay") === "true"; 351 352 // If autoplay is enabled, don't show play button 353 if (autoplay) { 354 return; 355 } 356 357 // Handle play button click 358 playButton.addEventListener("click", function () { 359 const src = videoIframe.src; 360 const shouldLoop = videoIframe.getAttribute("data-loop") === "true"; 361 362 // Hide the play button overlay 363 playButtonOverlay.classList.add("hidden"); 364 365 // Recreate the iframe URL with autoplay enabled 366 let newSrc = src; 367 368 if (src.includes("youtube.com")) { 369 // YouTube: add/update autoplay and mute parameters 370 newSrc = updateUrlParameter(src, "autoplay", "1"); 371 newSrc = updateUrlParameter(newSrc, "mute", "1"); 372 373 // Add loop parameter if enabled 374 if (shouldLoop) { 375 newSrc = updateUrlParameter(newSrc, "loop", "1"); 376 // YouTube requires playlist parameter for loop to work 377 const videoId = extractYouTubeVideoId(src); 378 if (videoId) { 379 newSrc = updateUrlParameter(newSrc, "playlist", videoId); 380 } 381 } 382 } else if (src.includes("vimeo.com")) { 383 // Vimeo: add/update autoplay and muted parameters 384 newSrc = updateUrlParameter(src, "autoplay", "1"); 385 newSrc = updateUrlParameter(newSrc, "muted", "1"); 386 387 // Add loop parameter if enabled 388 if (shouldLoop) { 389 newSrc = updateUrlParameter(newSrc, "loop", "1"); 390 } 391 } else if (src.includes("cloudflarestream.com")) { 392 // Cloudflare Stream: add/update autoplay and muted parameters 393 newSrc = updateUrlParameter(src, "autoplay", "true"); 394 newSrc = updateUrlParameter(newSrc, "muted", "true"); 395 396 // Add loop parameter if enabled 397 if (shouldLoop) { 398 newSrc = updateUrlParameter(newSrc, "loop", "true"); 399 } 400 } 401 402 // Update iframe src to trigger video playback 403 videoIframe.src = newSrc; 404 405 // Make video visible if it was hidden 406 videoIframe.style.opacity = "1"; 407 videoIframe.classList.add("fade-in"); 408 }); 409} 410 411/** 412 * Helper function to update URL parameters 413 * @param {string} url - The URL to update 414 * @param {string} param - The parameter name 415 * @param {string} value - The parameter value 416 * @returns {string} Updated URL 417 */ 418function updateUrlParameter(url, param, value) { 419 const urlObj = new URL(url); 420 urlObj.searchParams.set(param, value); 421 return urlObj.toString(); 422} 423 424/** 425 * Extract YouTube video ID from embed URL 426 * @param {string} url - The YouTube embed URL 427 * @returns {string|null} Video ID or null 428 */ 429function extractYouTubeVideoId(url) { 430 try { 431 const urlObj = new URL(url); 432 // Check if playlist parameter already exists 433 const existingPlaylist = urlObj.searchParams.get("playlist"); 434 if (existingPlaylist) { 435 return existingPlaylist; 436 } 437 // Extract from /embed/VIDEO_ID format 438 const pathMatch = urlObj.pathname.match(/\/embed\/([^/?]+)/); 439 if (pathMatch && pathMatch[1]) { 440 return pathMatch[1]; 441 } 442 } catch (e) { 443 console.error("Error extracting YouTube video ID:", e); 444 } 445 return null; 446}
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.