PageSourceSearch

https://tudorhousehotel.co.uk/wp-content/themes/redcat/template-pa…/floating-menu/floating-menu.js?ver=1.3.8

js tudorhousehotel.co.uk collected 2026-10-08 18:49:56 UTC 10,383 bytes, 366 lines download raw bytes

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">&times;</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.