1(function () { 2 const MOBILE_BREAKPOINT = 768; 3 4 class StudyPlan { 5 constructor(container) { 6 this.container = container; 7 this.modules = this.container.querySelectorAll('.module'); 8 this.headers = this.container.querySelectorAll('.header'); 9 10 this.init(); 11 } 12 13 init() { 14 this.buildCategoryFilter(); 15 this.initCategoryFilter(); 16 this.initModuleClicks(); 17 this.initModal(); 18 this.handleResize(); 19 this.initHeaderClicks(); 20 } 21 22 buildCategoryFilter() { 23 const filterList = this.container.querySelector('.filter'); 24 const filterTemplate = this.container.querySelector('.filter li')?.outerHTML; 25 26 if (!filterList || !filterTemplate) return; 27 28 filterList.innerHTML = ''; 29 30 const categoriesMap = new Map(); 31 32 this.container.querySelectorAll('.module[data-categories]').forEach(module => { 33 const categoriesData = module.dataset.categories; 34 if (!categoriesData || categoriesData === '[]') return; 35 36 try { 37 const categories = JSON.parse(categoriesData); 38 categories.forEach(cat => { 39 if (cat.uid && !categoriesMap.has(cat.uid)) { 40 categoriesMap.set(cat.uid, { 41 uid: cat.uid, 42 label: cat.label, 43 colour: cat.colour 44 }); 45 } 46 }); 47 } catch (e) { } 48 }); 49 50 if (categoriesMap.size === 0) { 51 return; 52 } 53 54 Array.from(categoriesMap.values()).forEach(category => { 55 let template = filterTemplate; 56 57 template = template.replace(/category-id-placeholder/g, `${category.uid}`); 58 template = template.replace(/category-color-placeholder/g, `${category.colour}`); 59 template = template.replace(/category-label-placeholder/g, `${category.label}`); 60 61 let temp = document.createElement('li'); 62 temp.innerHTML = template; 63 64 filterList.appendChild(temp.firstElementChild); 65 }); 66 } 67 68 getModuleCategoryIds(module) { 69 const categoriesData = module.dataset.categories; 70 if (!categoriesData || categoriesData === '[]') return []; 71 72 try { 73 const categories = JSON.parse(categoriesData); 74 return categories.map(cat => String(cat.uid)); 75 } catch (e) { 76 return []; 77 } 78 } 79 80 initCategoryFilter() { 81 this.container.querySelectorAll('.filter button').forEach(button => { 82 button.addEventListener('click', (e) => { 83 if (button.classList.contains('highlighted')) { 84 this.clearHighlights(); 85 } else { 86 this.highlightCategory(button.dataset.categoryId, button.dataset.categoryColor); 87 button.classList.add('highlighted'); 88 } 89 }); 90 }); 91 } 92 93 highlightCategory(categoryId, color) { 94 this.clearHighlights(); 95 this.container.classList.add('highlighted'); 96 97 this.modules.forEach(module => { 98 const categoryIds = this.getModuleCategoryIds(module); 99 100 if (categoryIds.includes(categoryId)) { 101 module.classList.add('highlighted'); 102 module.closest('.col').classList.add('highlighted', 'open'); 103 this.container.style.setProperty('--highlight-color', this.hexToRgba(color, 0.25)); 104 } 105 }); 106 } 107 108 clearHighlights() { 109 this.container.querySelectorAll('.highlighted').forEach(highlighted => { 110 highlighted.classList.remove('highlighted', 'open'); 111 }); 112 this.container.classList.remove('highlighted'); 113 } 114 115 initHeaderClicks() { 116 this.headers.forEach(header => { 117 header.addEventListener('click', () => { 118 if (window.innerWidth <= MOBILE_BREAKPOINT) { 119 header.closest('.col').classList.toggle('open'); 120 header.setAttribute('aria-expanded', header.closest('.col').classList.contains('open')); 121 } 122 }); 123 }); 124 } 125 126 enableMobile() { 127 this.headers.forEach(header => { 128 header.style.height = ''; 129 header.setAttribute('aria-expanded', 'false'); 130 header.setAttribute('tabindex', '0'); 131 header.setAttribute('role', 'button'); 132 header.setAttribute('aria-hidden', 'false'); 133 header.removeAttribute('inert'); 134 }); 135
136 this.modules.forEach(module => { 137 module.style.height = ''; 138 }); 139 } 140 141 disableMobile() { 142 let headerHeight = 0; 143 let moduleHeight = 0; 144 145 this.headers.forEach(header => { 146 headerHeight = Math.max(headerHeight, header.offsetHeight); 147 }); 148 149 this.headers.forEach(header => { 150 header.style.height = `${headerHeight}px`; 151 header.setAttribute('tabindex', '-1'); 152 header.setAttribute('role', ''); 153 header.setAttribute('aria-hidden', 'true'); 154 header.setAttribute('inert', ''); 155 }); 156 157 this.modules.forEach(module => { 158 moduleHeight = Math.max(moduleHeight, module.offsetHeight); 159 }); 160 161 this.modules.forEach(module => { 162 module.style.height = `${moduleHeight}px`; 163 }); 164 } 165 166 initModuleClicks() { 167 this.container.querySelectorAll('.modal-trigger').forEach(module => { 168 module.addEventListener('click', () => document.getElementById(module.dataset.dialogId)?.showModal()); 169 }); 170 } 171 172 initModal() { 173 document.querySelectorAll('.module dialog').forEach(dialog => { 174 const button = dialog.querySelector('button'); 175 176 button.addEventListener('click', (e) => { 177 this.closeModal(dialog) 178 e.stopPropagation(); 179 }); 180 }); 181 } 182 183 closeModal(dialog) { 184 const audioElements = dialog.querySelectorAll('audio'); 185 186 audioElements.forEach(audio => { 187 audio.pause(); 188 audio.currentTime = 0; 189 }); 190 191 dialog.close(); 192 } 193 194 hexToRgba(hex, alpha) { 195 if (!hex || hex.length < 7) return `rgba(0, 0, 0, ${alpha})`; 196 const r = parseInt(hex.slice(1, 3), 16); 197 const g = parseInt(hex.slice(3, 5), 16); 198 const b = parseInt(hex.slice(5, 7), 16); 199 return `rgba(${r}, ${g}, ${b}, ${alpha})`; 200 } 201 202 handleResize() { 203 if (window.innerWidth > MOBILE_BREAKPOINT) { 204 this.disableMobile(); 205 } else { 206 this.enableMobile(); 207 } 208 } 209 } 210 211 const studyPlanInstances = new Map(); 212 213 function init() { 214 document.querySelectorAll('.academic-study-plan').forEach(container => { 215 const id = container.dataset.studyPlan; 216 217 if (!studyPlanInstances.has(id)) { 218 studyPlanInstances.set(id, new StudyPlan(container)); 219 } 220 }); 221 } 222 223 function handleResize() { 224 studyPlanInstances.forEach(instance => instance.handleResize()); 225 } 226 227 if (document.readyState === 'loading') { 228 document.addEventListener('DOMContentLoaded', init); 229 } else { 230 init(); 231 } 232 233 let resizeTimeout; 234 window.addEventListener('resize', () => { 235 clearTimeout(resizeTimeout); 236 resizeTimeout = setTimeout(handleResize, 150); 237 }); 238})();
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.