PageSourceSearch

https://bestforbritain.org/wp-content/themes/chd/js/modal.js?ver=1790086030

js bestforbritain.org collected 2026-09-24 09:22:24 UTC 5,535 bytes, 191 lines download raw bytes

1class Modal {
2  constructor() {
3    this.activeModal = null;
4    this.previousFocus = null;
5    this.init();
6  }
7
8  init() {
9    // Handle modal triggers
10    document.addEventListener("click", (e) => {
11      const trigger = e.target.closest("[data-modal]");
12      if (trigger) {
13        e.preventDefault();
14        const modalId = trigger.dataset.modal;
15        const modal = document.getElementById(modalId);
16        if (modal) {
17          this.openModal(modal, trigger);
18        }
19      }
20    });
21
22    // Handle modal close buttons
23    document.addEventListener("click", (e) => {
24      const closeBtn = e.target.closest(".modal-close");
25      if (closeBtn) {
26        const modal = closeBtn.closest(".modal");
27        if (modal) {
28          this.closeModal(modal);
29        }
30      }
31    });
32
33    // Handle overlay clicks
34    document.addEventListener("click", (e) => {
35      if (e.target.classList.contains("modal-overlay")) {
36        const modal = e.target.closest(".modal");
37        if (modal) {
38          this.closeModal(modal);
39        }
40      }
41    });
42
43    // Handle escape key
44    document.addEventListener("keydown", (e) => {
45      if (e.key === "Escape" && this.activeModal) {
46        this.closeModal(this.activeModal);
47      }
48    });
49
50    // Handle gallery navigation
51    document.addEventListener("click", (e) => {
52      const prevBtn = e.target.closest(".gallery-prev");
53      const nextBtn = e.target.closest(".gallery-next");
54
55      if (prevBtn || nextBtn) {
56        const modal = e.target.closest(".modal");
57        if (modal) {
58          this.navigateGallery(modal, prevBtn ? "prev" : "next");
59        }
60      }
61    });
62
63    // Handle keyboard navigation in gallery
64    document.addEventListener("keydown", (e) => {
65      if (
66        !this.activeModal ||
67        !this.activeModal.classList.contains("gallery-modal")
68      )
69        return;
70
71      if (e.key === "ArrowLeft") {
72        this.navigateGallery(this.activeModal, "prev");
73      } else if (e.key === "ArrowRight") {
74        this.navigateGallery(this.activeModal, "next");
75      }
76    });
77  }
78
79  openModal(modal, trigger) {
80    this.activeModal = modal;
81    this.previousFocus = document.activeElement;
82
83    // Set initial state
84    modal.setAttribute("aria-hidden", "false");
85    modal.style.display = "flex";
86    document.body.classList.add("modal-open");
87
88    // If this is a gallery modal and we have a trigger with an index
89    if (modal.classList.contains("gallery-modal") && trigger.dataset.index) {
90      this.showGalleryItem(modal, parseInt(trigger.dataset.index));
91    }
92
93    // Focus trap
94    this.setupFocusTrap(modal);
95
96    // Announce to screen readers
97    this.announceModal(modal);
98  }
99
100  closeModal(modal) {
101    modal.setAttribute("aria-hidden", "true");
102    modal.style.display = "none";
103    document.body.classList.remove("modal-open");
104
105    // Return focus to previous element
106    if (this.previousFocus) {
107      this.previousFocus.focus();
108    }
109
110    this.activeModal = null;
111  }
112
113  setupFocusTrap(modal) {
114    const focusableElements = modal.querySelectorAll(
115      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
116    );
117    const firstFocusable = focusableElements[0];
118    const lastFocusable = focusableElements[focusableElements.length - 1];
119
120    modal.addEventListener("keydown", (e) => {
121      if (e.key !== "Tab") return;
122
123      if (e.shiftKey) {
124        if (document.activeElement === firstFocusable) {
125          lastFocusable.focus();
126          e.preventDefault();
127        }
128      } else {
129        if (document.activeElement === lastFocusable) {
130          firstFocusable.focus();
131          e.preventDefault();
132        }
133      }
134    });
135
136    // Focus first element
137    firstFocusable.focus();
138  }
139
140  navigateGallery(modal, direction) {
141    const items = modal.querySelectorAll(".gallery-item");
142    const currentItem = modal.querySelector(".gallery-item.active");
143    const currentIndex = Array.from(items).indexOf(currentItem);
144
145    let newIndex;
146    if (direction === "prev") {
147      newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
148    } else {
149      newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
150    }
151
152    this.showGalleryItem(modal, newIndex);
153  }
154
155  showGalleryItem(modal, index) {
156    const items = modal.querySelectorAll(".gallery-item");
157    items.forEach((item) => item.classList.remove("active"));
158    items[index].classList.add("active");
159
160    // Update ARIA live region
161    const total = items.length;
162    const current = index + 1;
163    this.announceGalleryNavigation(modal, current, total);
164  }
165
166  announceModal(modal) {
167    const title = modal.querySelector(".modal-title");
168    if (title) {
169      const announcement = document.createElement("div");
170      announcement.setAttribute("aria-live", "polite");
171      announcement.setAttribute("class", "sr-only");
172      announcement.textContent = `Modal opened: ${title.textContent}`;
173      document.body.appendChild(announcement);
174      setTimeout(() => announcement.remove(), 1000);
175    }
176  }
177
178  announceGalleryNavigation(modal, current, total) {
179    const announcement = document.createElement("div");
180    announcement.setAttribute("aria-live", "polite");
181    announcement.setAttribute("class", "sr-only");
182    announcement.textContent = `Image ${current} of ${total}`;
183    document.body.appendChild(announcement);
184    setTimeout(() => announcement.remove(), 1000);
185  }
186}
187
188// Initialize modal functionality
189document.addEventListener("DOMContentLoaded", () => {
190  window.chdModal = new Modal();
191});

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.