1/* global slideshow */ 2(function () { 3 const ready = function (fn) { 4 /* MIT License Copyright (c) 2016 Nuclei */ 5 /* https://github.com/nuclei/readyjs */ 6 const completed = () => { 7 document.removeEventListener('DOMContentLoaded', completed) 8 window.removeEventListener('load', completed) 9 fn() 10 } 11 if (document.readyState !== 'loading') { 12 setTimeout(fn) 13 } else { 14 document.addEventListener('DOMContentLoaded', completed) 15 window.addEventListener('load', completed) 16 } 17 } 18 19 ready(function () { 20 [...document.querySelectorAll('.panel-name')] 21 .map(el => el.textContent.trim()) 22 23 const panelIds = {} 24 25 const uniquePanelId = (name) => { 26 name = encodeURIComponent(name.toLowerCase().replace(/[\s]/g, '-')) 27 if (Object.keys(panelIds).includes(name)) { 28 name += ++panelIds[name] 29 } else { 30 panelIds[name] = 1 31 } 32 return name 33 } 34 35 const identifyPanelName = (item) => { 36 let name = 'Panel' 37 38 // In R Markdown when header-attrs.js is present, we may have found a 39 // section header but the class attributes won't be duplicated on the <hX> tag 40 if ( 41 (item.tagName === 'SECTION' || item.classList.contains('section')) && 42 /^H[1-6]/.test(item.children[0].tagName) 43 ) { 44 name = item.children[0].textContent 45 item.classList.remove('panel-name') 46 item.removeChild(item.children[0]) 47 return name 48 } 49 50 const nameDiv = item.querySelector('.panel-name') 51 if (!nameDiv) return name 52 53 // In remarkjs the .panel-name span might be in a paragraph tag 54 // and if the <p> is empty, we'll remove it 55 if ( 56 nameDiv.tagName === 'SPAN' && 57 nameDiv.parentNode.tagName === 'P' && 58 nameDiv.textContent === nameDiv.parentNode.textContent 59 ) { 60 name = nameDiv.textContent 61 item.removeChild(nameDiv.parentNode) 62 return name 63 } 64 65 // If none of the above, remove the nameDiv and return the name 66 name = nameDiv.textContent 67 nameDiv.parentNode.removeChild(nameDiv) 68 return name 69 } 70 71 const processPanelItem = (item) => { 72 const name = identifyPanelName(item) 73 return { name, content: item.children, id: uniquePanelId(name) } 74 } 75 76 const getCurrentPanelFromUrl = (panelset) => { 77 const params = new URLSearchParams(window.location.search) 78 return params.get(panelset) 79 } 80 81 const reflowPanelSet = (panels, idx) => { 82 const res = document.createElement('div') 83 res.className = 'panelset' 84 res.id = 'panelset' + (idx > 0 ? idx : '') 85 const panelSelected = getCurrentPanelFromUrl(res.id) 86 87 // create header row 88 const headerRow = document.createElement('ul') 89 headerRow.className = 'panel-tabs' 90 headerRow.setAttribute('role', 'tablist') 91 panels 92 .map((p, idx) => { 93 const panelHeaderItem = document.createElement('li') 94 panelHeaderItem.className = 'panel-tab' 95 panelHeaderItem.setAttribute('role', 'tab') 96 const thisPanelIsActive = panelSelected ? panelSelected === p.id : idx === 0 97 if (thisPanelIsActive) { 98 panelHeaderItem.classList.add('panel-tab-active') 99 panelHeaderItem.setAttribute('aria-selected', true) 100 } 101 panelHeaderItem.tabIndex = 0 102 panelHeaderItem.id = res.id + '_' + p.id // #panelsetid_panelid 103 104 const panelHeaderLink = document.createElement('a') 105 panelHeaderLink.href = '?' + res.id + '=' + p.id + '#' + panelHeaderItem.id 106 panelHeaderLink.setAttribute('onclick', 'return false;') 107 panelHeaderLink.tabIndex = -1 // list item is tabable, not link 108 panelHeaderLink.innerHTML = p.name 109 panelHeaderLink.setAttribute('aria-controls', p.id) 110 111 panelHeaderItem.appendChild(panelHeaderLink) 112 return panelHeaderItem 113 }) 114 .forEach(el => headerRow.appendChild(el)) 115 116 res.appendChild(headerRow) 117 118 panels 119 .map((p, idx) => { 120 const panelContent = document.createElement('section') 121 panelContent.className = 'panel' 122 panelContent.setAttribute('role', 'tabpanel') 123 const thisPanelIsActive = panelSelected ? panelSelected === p.id : idx === 0 124 panelContent.classList.toggle('panel-active', thisPanelIsActive) 125 panelContent.id = p.id 126 panelContent.setAttribute('aria-labelledby', p.id) 127 Array.from(p.content).forEach(el => panelContent.appendChild(el)) 128 return panelContent 129 }) 130 .forEach(el => res.appendChild(el)) 131 132 return res 133 } 134 135 const updateUrl = (panelset, panel) => { 136 let params = new URLSearchParams(window.location.search) 137 if (panel) { 138 params.set(panelset, panel) 139 } else { 140 params.delete(panelset) 141 } 142 params = params.toString() ? ('?' + params.toString()) : '' 143 const { pathname, hash } = window.location 144 const uri = pathname + params + hash 145 window.history.replaceState(uri, '', uri) 146 } 147 148 const togglePanel = (clicked) => { 149 if (clicked.nodeName.toUpperCase() === 'A') { 150 clicked = clicked.parentElement 151 } 152 if (!clicked.classList.contains('panel-tab')) return 153 if (clicked.classList.contains('panel-tab-active')) return 154 155 const tabs = clicked.parentNode 156 .querySelectorAll('.panel-tab') 157 const panels = clicked.parentNode.parentNode 158 .querySelectorAll('.panel') 159 const panelTabClicked = clicked.children[0].getAttribute('aria-controls') 160 const panelClicked = clicked.parentNode.parentNode.id 161 162 Array.from(tabs) 163 .forEach(t => { 164 t.classList.remove('panel-tab-active') 165 t.removeAttribute('aria-selected') 166 }) 167 Array.from(panels) 168 .forEach(p => { 169 const active = p.id === panelTabClicked 170 p.classList.toggle('panel-active', active) 171 // make inactive panels inaccessible by keyboard navigation 172 if (active) { 173 p.removeAttribute('tabIndex') 174 p.removeAttribute('aria-hidden') 175 } else { 176 p.setAttribute('tabIndex', -1) 177 p.setAttribute('aria-hidden', true) 178 } 179 }) 180 181 clicked.classList.add('panel-tab-active') 182 clicked.setAttribute('aria-selected', true) 183 184 // update query string 185 updateUrl(panelClicked, panelTabClicked) 186 } 187 188 const initPanelSet = (panelset, idx) => { 189 const panels = Array.from(panelset.querySelectorAll('.panel')) 190 if (!panels.length) return 191 192 const contents = panels.map(processPanelItem) 193 const newPanelSet = reflowPanelSet(contents, idx) 194 panelset.parentNode.insertBefore(newPanelSet, panelset) 195 panelset.parentNode.removeChild(panelset) 196 197 // click and touch events 198 const panelTabs = newPanelSet.querySelector('.panel-tabs'); 199 ['click', 'touchend'].forEach(eventType => { 200 panelTabs.addEventListener(eventType, function (ev) { 201 togglePanel(ev.target) 202 ev.stopPropagation() 203 }) 204 }) 205 panelTabs.addEventListener('touchmove', function (ev) { 206 ev.preventDefault() 207 }) 208 209 // key events 210 newPanelSet 211 .querySelector('.panel-tabs') 212 .addEventListener('keydown', (ev) => { 213 const self = ev.currentTarget.querySelector('.panel-tab-active') 214 if (ev.code === 'Space' || ev.code === 'Enter') { 215 togglePanel(ev.target) 216 ev.stopPropagation() 217 } else if (ev.code === 'ArrowLeft' && self.previousSibling) { 218 togglePanel(self.previousSibling) 219 self.previousSibling.focus() 220 ev.stopPropagation() 221 } else if (ev.code === 'ArrowRight' && self.nextSibling) { 222 togglePanel(self.nextSibling) 223 self.nextSibling.focus() 224 ev.stopPropagation() 225 } 226 }) 227 228 return panels 229 } 230 231 // initialize panels 232 Array.from(document.querySelectorAll('.panelset')).map(initPanelSet) 233 234 if (typeof slideshow !== 'undefined') { 235 const getVisibleActivePanelInfo = () => { 236 const slidePanels = document.querySelectorAll('.remark-visible .panel-tab-active') 237 238 if (!slidePanels.length) return null 239 240 return slidePanels.map(panel => { 241 return { 242 panel, 243 panelId: panel.children[0].getAttribute('aria-controls'), 244 panelSetId: panel.parentNode.parentNode.id 245 } 246 }) 247 } 248 249 slideshow.on('hideSlide', slide => { 250 // clear focus if we had a panel-tab selected 251 document.activeElement.blur() 252 253 // clear search query for panelsets in current slide 254 document.querySelectorAll('.remark-visible .panelset') 255 .forEach(ps => updateUrl(ps.id, null)) 256 }) 257 258 slideshow.on('afterShowSlide', slide => { 259 const slidePanels = getVisibleActivePanelInfo() 260 261 if (slidePanels) {
262 // only first panel gets focus 263 slidePanels[0].panel.focus() 264 // but still update the url to reflect all active panels 265 slidePanels.forEach(({ panelId, panelSetId }) => updateUrl(panelSetId, panelId)) 266 } 267 }) 268 } 269 }) 270})()
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.