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.