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