1// Filter panels open as a modal dialog, so they stay usable after scrolling. 2// Shared by the generic .ps-toggle-filters wiring below and the [programme] 3// shortcode's own toolbar script. 4window.psFilterModal = (function() { 5 let backdrop = null; 6 let activePanel = null; 7 let activeButton = null; 8 9 function ensureCloseButton(panel) { 10 const header = panel.querySelector('.filter-header'); 11 if (!header || header.querySelector('.ps-modal-close')) { 12 return; 13 } 14 const button = document.createElement('button'); 15 button.type = 'button'; 16 button.className = 'ps-modal-close'; 17 button.setAttribute('aria-label', 'Close'); 18 button.innerHTML = '×'; 19 button.addEventListener('click', close); 20 header.appendChild(button); 21 } 22 23 function onKeydown(event) { 24 if (event.key === 'Escape') { 25 close(); 26 } 27 } 28 29 function open(panel, button) { 30 close(); 31 activePanel = panel; 32 activeButton = button || null; 33 ensureCloseButton(panel); 34 backdrop = document.createElement('div'); 35 backdrop.className = 'ps-modal-backdrop'; 36 backdrop.addEventListener('click', close); 37 document.body.appendChild(backdrop); 38 panel.removeAttribute('hidden'); 39 panel.classList.add('ps-modal-open'); 40 panel.setAttribute('role', 'dialog'); 41 panel.setAttribute('aria-modal', 'true'); 42 document.body.classList.add('ps-modal-active'); 43 document.addEventListener('keydown', onKeydown); 44 if (activeButton) { 45 activeButton.setAttribute('aria-expanded', 'true'); 46 } 47 } 48 49 function close() { 50 if (!activePanel) { 51 return; 52 } 53 activePanel.classList.remove('ps-modal-open'); 54 activePanel.setAttribute('hidden', ''); 55 activePanel.removeAttribute('role'); 56 activePanel.removeAttribute('aria-modal'); 57 if (activeButton) { 58 activeButton.setAttribute('aria-expanded', 'false'); 59 } 60 if (backdrop) { 61 backdrop.remove(); 62 backdrop = null; 63 } 64 document.body.classList.remove('ps-modal-active'); 65 document.removeEventListener('keydown', onKeydown); 66 activePanel = null; 67 activeButton = null; 68 } 69 70 function toggle(panel, button) { 71 if (activePanel === panel) { 72 close(); 73 } else { 74 open(panel, button); 75 } 76 } 77 78 return { open: open, close: close, toggle: toggle }; 79})(); 80 81document.addEventListener('DOMContentLoaded', function() { 82 const ytCheckers = document.querySelectorAll('.yt-checker'); 83 ytCheckers.forEach(function(checker) { 84 checker.addEventListener('click', function() { 85 const youtubeLink = checker.getAttribute('data-youtube-link'); 86 if (youtubeLink) { 87 const iframe = document.createElement('iframe'); 88 iframe.setAttribute('src', youtubeLink.replace('watch?v=', 'embed/')); 89 iframe.setAttribute('frameborder', '0'); 90 iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); 91 iframe.setAttribute('allowfullscreen', 'true'); 92 iframe.style.width = '100%'; 93 iframe.style.height = '100%'; 94 checker.innerHTML = ''; 95 checker.appendChild(iframe); 96 checker.style.position = 'relative'; 97 checker.style.paddingBottom = '56.25%'; 98 checker.style.height = '0'; 99 iframe.style.position = 'absolute'; 100 iframe.style.top = '0'; 101 iframe.style.left = '0'; 102 iframe.style.width = '100%'; 103 iframe.style.height = '100%'; 104 } 105 }); 106 }); 107 108 // Search functionality for session list 109 const sessionSearchInput = document.getElementById('session-search-input'); 110 if (sessionSearchInput) { 111 sessionSearchInput.addEventListener('input', function() { 112 const searchQuery = this.value.toLowerCase(); 113 const sessions = document.querySelectorAll('.session-item'); 114 115 sessions.forEach(function(session) { 116 const title = session.querySelector('h3').innerText.toLowerCase(); 117 const speakers = session.querySelector('.session-speakers') ? session.querySelector('.session-speakers').innerText.toLowerCase() : ''; 118 const room = session.querySelector('.session-room') ? session.querySelector('.session-room').innerText.toLowerCase() : ''; 119 const excerpt = session.querySelector('.session-excerpt') ? session.querySelector('.session-excerpt').innerText.toLowerCase() : ''; 120
121 if (title.includes(searchQuery) || speakers.includes(searchQuery) || room.includes(searchQuery) || excerpt.includes(searchQuery)) { 122 session.style.display = 'block'; 123 } else { 124 session.style.display = 'none'; 125 } 126 }); 127 }); 128 } 129 130 // Search functionality for speaker list 131 const speakerSearchInput = document.getElementById('speaker-search-input'); 132 if (speakerSearchInput) { 133 speakerSearchInput.addEventListener('input', function() { 134 const searchQuery = this.value.toLowerCase(); 135 const speakers = document.querySelectorAll('.speaker-item'); 136 137 speakers.forEach(function(speaker) { 138 const name = speaker.querySelector('h2').innerText.toLowerCase(); 139 140 if (name.includes(searchQuery)) { 141 speaker.style.display = 'block'; 142 } else { 143 speaker.style.display = 'none'; 144 } 145 }); 146 }); 147 } 148 149 // Search functionality for organisation list 150 const orgSearchInput = document.getElementById('organisation-search-input'); 151 if (orgSearchInput) { 152 orgSearchInput.addEventListener('input', function() { 153 const searchQuery = this.value.toLowerCase(); 154 const orgs = document.querySelectorAll('.organisation-item'); 155 156 orgs.forEach(function(org) { 157 const name = org.querySelector('h2').innerText.toLowerCase(); 158 159 if (name.includes(searchQuery)) { 160 org.style.display = ''; 161 } else { 162 org.style.display = 'none'; 163 } 164 }); 165 }); 166 } 167 168 // Standalone favorite hearts (single session view): same localStorage 169 // list as the schedule views, announced with the same event. 170 document.querySelectorAll('.ps-favorite-standalone').forEach(function(star) { 171 const sessionId = star.getAttribute('data-session-id'); 172 if (!sessionId) { 173 return; 174 } 175 function readLiked() { 176 try { 177 return new Set(JSON.parse(localStorage.getItem('likedSessions') || '[]')); 178 } catch (e) { 179 return new Set(); 180 } 181 } 182 function render(liked) { 183 const isLiked = liked.has(sessionId); 184 star.classList.toggle('favorited', isLiked); 185 const title = star.getAttribute(isLiked ? 'data-title-remove' : 'data-title-add'); 186 if (title) { 187 star.title = title; 188 } 189 } 190 render(readLiked()); 191 star.addEventListener('click', function(event) { 192 event.preventDefault(); 193 const liked = readLiked(); 194 if (liked.has(sessionId)) { 195 liked.delete(sessionId); 196 } else { 197 liked.add(sessionId); 198 } 199 try { 200 localStorage.setItem('likedSessions', JSON.stringify(Array.from(liked))); 201 } catch (e) { 202 return; 203 } 204 render(liked); 205 star.dispatchEvent(new CustomEvent('programmesyncLikedChanged', { bubbles: true })); 206 }); 207 }); 208 209 // Collapsible filter panels: a .ps-toggle-filters button in a .ps-toolbar 210 // opens/closes the next .filter-container sibling of that toolbar. 211 // (The [programme] shortcode wires its own toggle and is not affected.) 212 document.querySelectorAll('.ps-toggle-filters').forEach(function(button) { 213 const toolbar = button.closest('.ps-toolbar'); 214 let panel = toolbar ? toolbar.nextElementSibling : null; 215 while (panel && !(panel.classList && panel.classList.contains('filter-container'))) { 216 panel = panel.nextElementSibling; 217 } 218 if (!panel) { 219 return; 220 } 221 button.addEventListener('click', function() { 222 window.psFilterModal.toggle(panel, button); 223 }); 224 }); 225});
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.