1/** 2 * Floating Menu Block Frontend JavaScript 3 * 4 * Handles the floating menu interactions including hover animations and click actions. 5 */ 6 7(function () { 8 "use strict"; 9 10 // Main floating menu handler 11 const FloatingMenu = { 12 init: function () { 13 this.bindEvents(); 14 }, 15 16 bindEvents: function () { 17 document.addEventListener("DOMContentLoaded", () => { 18 this.setupFloatingMenu(); 19 }); 20 }, 21 22 setupFloatingMenu: function () { 23 // Find all floating menu containers on the page 24 const menuContainers = document.querySelectorAll( 25 ".redcat-floating-menu:not(.floating-menu-preview)" 26 ); 27 28 menuContainers.forEach((container) => { 29 const config = this.getMenuConfig(container); 30 if (!config || !config.enabled) { 31 return; 32 } 33 34 this.initializeMenu(container, config); 35 }); 36 }, 37 38 getMenuConfig: function (container) { 39 const configScript = container.querySelector(".floating-menu-config"); 40 if (!configScript) return null; 41 42 try { 43 return JSON.parse(configScript.textContent); 44 } catch (e) { 45 console.warn("Failed to parse floating menu config:", e); 46 return null; 47 } 48 }, 49 50 initializeMenu: function (container, config) { 51 const menuItems = container.querySelectorAll(".floating-menu-item"); 52 53 if (!menuItems.length) return; 54 55 // Make menu globally visible 56 this.makeMenuGlobal(container, config); 57 58 // Setup hover animations 59 this.setupHoverAnimations(container); 60 61 // Setup click handlers 62 this.setupClickHandlers(menuItems); 63 64 // Setup accessibility 65 this.setupAccessibility(container); 66 }, 67 68 makeMenuGlobal: function (container, config) { 69 // Clone the menu and append to body for global positioning 70 const globalMenu = container.cloneNode(true); 71 globalMenu.classList.add("floating-menu-global"); 72 globalMenu.style.position = "fixed"; 73 globalMenu.style.zIndex = "9999"; 74 75 console.log( 76 "Setting menu position: xxxx ------------------->", 77 config.position 78 ); 79 80 // Set position based on config 81 this.setMenuPosition(globalMenu, config.position); 82 83 // Hide the original menu (keep it for block editor) 84 container.style.display = "none"; 85 86 // Append to body 87 document.body.appendChild(globalMenu); 88 89 // Re-setup functionality for the global menu 90 const globalMenuItems = globalMenu.querySelectorAll( 91 ".floating-menu-item" 92 ); 93 this.setupHoverAnimations(globalMenu); 94 this.setupClickHandlers(globalMenuItems); 95 this.setupAccessibility(globalMenu); 96 }, 97 98 // Reset all position styles 99 setMenuPosition: function (menu, position) { 100 console.log("Setting menu position: ------------------->", position); 101 102 menu.style.top = "auto"; 103 menu.style.bottom = "auto"; 104 menu.style.left = "auto"; 105 menu.style.right = "auto"; 106 107 switch (position) { 108 case "top-left": 109 menu.style.top = "20px"; 110 menu.style.left = "20px"; 111 break; 112 case "top-right": 113 menu.style.top = "20px"; 114 menu.style.right = "20px"; 115 break; 116 case "bottom-left": 117 menu.style.bottom = "20px"; 118 menu.style.left = "20px"; 119 break; 120 case "bottom-right": 121 menu.style.bottom = "20px"; 122 menu.style.right = "20px"; 123 break; 124 default: 125 menu.style.top = "20px"; 126 menu.style.right = "20px"; 127 } 128 }, 129 130 setupHoverAnimations: function (container) { 131 const menuContainer = container.querySelector(".floating-menu-container"); 132 if (!menuContainer) return; 133 134 menuContainer.addEventListener("mouseenter", () => { 135 menuContainer.classList.add("floating-menu-expanded"); 136 }); 137 138 menuContainer.addEventListener("mouseleave", () => { 139 menuContainer.classList.remove("floating-menu-expanded"); 140 }); 141 }, 142 143 setupClickHandlers: function (menuItems) { 144 menuItems.forEach((item) => { 145 const button = item.querySelector(".floating-menu-button"); 146 if (!button) return; 147 148 button.addEventListener("click", (e) => { 149 e.preventDefault(); 150 this.handleMenuClick(item); 151 }); 152 }); 153 }, 154 155 setupAccessibility: function (container) { 156 const buttons = container.querySelectorAll(".floating-menu-button"); 157 158 buttons.forEach((button) => { 159 // Add keyboard navigation 160 button.addEventListener("keydown", (e) => { 161 if (e.key === "Enter" || e.key === " ") { 162 e.preventDefault(); 163 button.click(); 164 } 165 }); 166 167 // Ensure focusable 168 if (!button.hasAttribute("tabindex")) { 169 button.setAttribute("tabindex", "0"); 170 } 171 }); 172 }, 173 174 handleMenuClick: function (menuItem) { 175 const actionType = menuItem.dataset.actionType; 176 const button = menuItem.querySelector(".floating-menu-button"); 177 178 // Add loading state 179 this.setButtonLoading(button, true); 180 181 // Handle different action types 182 switch (actionType) { 183 case "redirect": 184 this.handleRedirect(menuItem); 185 break; 186 case "download": 187 this.handleDownload(menuItem); 188 break; 189 case "popup": 190 this.handlePopup(menuItem); 191 break; 192 default: 193 console.warn("Unknown action type:", actionType); 194 } 195 196 // Remove loading state after delay 197 setTimeout(() => { 198 this.setButtonLoading(button, false); 199 }, 300); 200 201 // Track analytics 202 this.trackMenuClick(actionType, menuItem); 203 }, 204 205 handleRedirect: function (menuItem) { 206 const url = menuItem.dataset.redirectUrl; 207 const target = menuItem.dataset.redirectTarget || "_self"; 208 209 if (!url) { 210 console.warn("No redirect URL specified"); 211 return; 212 } 213 214 if (target === "_blank") { 215 window.open(url, "_blank", "noopener,noreferrer"); 216 } else { 217 window.location.href = url; 218 } 219 }, 220 221 handleDownload: function (menuItem) { 222 const url = menuItem.dataset.downloadUrl; 223 const filename = menuItem.dataset.downloadFilename; 224 225 if (!url) { 226 console.warn("No download URL specified"); 227 return; 228 } 229 230 // Create temporary link for download 231 const link = document.createElement("a"); 232 link.href = url; 233 link.download = filename || ""; 234 link.style.display = "none"; 235 236 document.body.appendChild(link); 237 link.click(); 238 document.body.removeChild(link); 239 }, 240 241 handlePopup: function (menuItem) { 242 const title = menuItem.dataset.popupTitle || "Information"; 243 const contentElement = menuItem.querySelector( 244 ".floating-menu-popup-content" 245 ); 246 247 if (!contentElement) { 248 console.warn("No popup content found"); 249 return; 250 } 251 252 const content = contentElement.innerHTML; 253 this.showPopup(title, content); 254 }, 255 256 showPopup: function (title, content) { 257 // Create popup overlay
258 const overlay = document.createElement("div"); 259 overlay.className = "floating-menu-popup-overlay"; 260 overlay.innerHTML = ` 261 <div class="floating-menu-popup-modal"> 262 <div class="floating-menu-popup-header"> 263 <h3 class="floating-menu-popup-title">${this.escapeHtml(title)}</h3> 264 <button class="floating-menu-popup-close" type="button" aria-label="Close"> 265 <span aria-hidden="true">×</span> 266 </button> 267 </div> 268 <div class="floating-menu-popup-content"> 269 ${content} 270 </div> 271 </div> 272 `; 273 274 // Add to body 275 document.body.appendChild(overlay); 276 277 // Setup close handlers 278 const closeBtn = overlay.querySelector(".floating-menu-popup-close"); 279 const modal = overlay.querySelector(".floating-menu-popup-modal"); 280 281 closeBtn.addEventListener("click", () => this.closePopup(overlay)); 282 overlay.addEventListener("click", (e) => { 283 if (e.target === overlay) { 284 this.closePopup(overlay); 285 } 286 }); 287 288 // Keyboard support 289 document.addEventListener("keydown", (e) => { 290 if (e.key === "Escape") { 291 this.closePopup(overlay); 292 } 293 }); 294 295 // Focus management 296 setTimeout(() => { 297 closeBtn.focus(); 298 }, 100); 299 300 // Prevent body scroll 301 document.body.style.overflow = "hidden"; 302 }, 303 304 closePopup: function (overlay) { 305 if (overlay && overlay.parentNode) { 306 overlay.parentNode.removeChild(overlay); 307 document.body.style.overflow = ""; 308 } 309 }, 310 311 setButtonLoading: function (button, isLoading) { 312 if (!button) return; 313 314 if (isLoading) { 315 button.classList.add("loading"); 316 button.disabled = true; 317 } else { 318 button.classList.remove("loading"); 319 button.disabled = false; 320 } 321 }, 322 323 escapeHtml: function (text) { 324 const div = document.createElement("div"); 325 div.textContent = text; 326 return div.innerHTML; 327 }, 328 329 trackMenuClick: function (actionType, menuItem) { 330 const itemIndex = menuItem.dataset.itemIndex; 331 const itemName = 332 menuItem.querySelector(".floating-menu-text-content")?.textContent || 333 "Unknown"; 334 335 // Track with Google Analytics if available 336 if (typeof gtag !== "undefined") { 337 gtag("event", "floating_menu_click", { 338 action_type: actionType, 339 item_name: itemName, 340 item_index: itemIndex, 341 event_category: "engagement", 342 event_label: `Floating Menu - ${actionType}`, 343 }); 344 } 345 346 // Track with Facebook Pixel if available 347 if (typeof fbq !== "undefined") { 348 fbq("track", "ViewContent", { 349 content_category: "floating_menu", 350 content_name: `${actionType}_${itemName}`, 351 }); 352 } 353 354 // Custom tracking hook for other analytics 355 if (typeof window.redcatFloatingMenuTrack === "function") { 356 window.redcatFloatingMenuTrack(actionType, itemName, itemIndex); 357 } 358 }, 359 }; 360 361 // Initialize when script loads 362 FloatingMenu.init(); 363 364 // Make it globally accessible for debugging 365 window.FloatingMenu = FloatingMenu; 366})();
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.