1/** 2 * Salem Events â month calendar behavior. 3 * 4 * Two jobs: the "Show ongoing groups" toggle, and the per-cell "+n more" 5 * expansion. Both are progressive enhancements â the grid is fully readable 6 * with JavaScript off, it just shows fewer entries. 7 * 8 * Listeners are bound directly to their elements rather than delegated, 9 * because Cornerstone intercepts clicks inside its containers. 10 */ 11( function () { 12 'use strict'; 13 14 var STORAGE_KEY = 'slmEventsHideOngoing'; 15 16 /** 17 * Whether ongoing groups should be hidden. 18 * 19 * Ongoing groups show by default; the checkbox opts out of them. 20 */ 21 function readPreference() { 22 try { 23 return window.sessionStorage.getItem( STORAGE_KEY ) === '1'; 24 } catch ( e ) { 25 // Private windows and blocked site data both throw here. 26 return false; 27 } 28 } 29 30 function writePreference( value ) { 31 try { 32 window.sessionStorage.setItem( STORAGE_KEY, value ? '1' : '0' ); 33 } catch ( e ) { 34 // Not being able to remember the choice is not worth failing over. 35 } 36 } 37 38 function announce( calendar, message ) { 39 var status = calendar.querySelector( '.slm-events-calendar__status' ); 40 41 if ( status ) { 42 status.textContent = message; 43 } 44 } 45 46 function applyOngoing( calendar, hide ) { 47 var entries = calendar.querySelectorAll( '[data-slm-ongoing="1"]' ); 48 var i; 49 50 for ( i = 0; i < entries.length; i++ ) { 51 entries[ i ].hidden = hide; 52 } 53 54 announce( 55 calendar, 56 hide ? 'Ongoing groups hidden.' : 'Ongoing groups shown.' 57 ); 58 } 59 60 function initToggle( calendar ) { 61 var input = calendar.querySelector( '.slm-events-calendar__toggle-input' ); 62 63 if ( ! input ) { 64 return; 65 } 66 67 var hidden = readPreference(); 68 69 input.checked = hidden; 70 applyOngoing( calendar, hidden ); 71 72 input.addEventListener( 'change', function () { 73 writePreference( input.checked ); 74 applyOngoing( calendar, input.checked ); 75 76 // The mobile agenda is rebuilt per month, so re-run the current 77 // month rather than only toggling what is on screen. 78 var current = calendar.getAttribute( 'data-slm-current' ); 79 80 if ( current ) { 81 renderMonth( calendar, current ); 82 } 83 } ); 84 } 85 86 function initMore( calendar ) { 87 var buttons = calendar.querySelectorAll( '.slm-events-calendar__more' ); 88 var i; 89 90 function makeHandler( button ) { 91 return function ( event ) { 92 var cell = button.parentNode; 93 var overflow = cell.querySelectorAll( '[data-slm-overflow="1"]' ); 94 var j; 95 96 event.preventDefault(); 97 event.stopPropagation(); 98 99 for ( j = 0; j < overflow.length; j++ ) { 100 overflow[ j ].hidden = false; 101 } 102 103 button.parentNode.removeChild( button ); 104 }; 105 } 106 107 for ( i = 0; i < buttons.length; i++ ) { 108 buttons[ i ].addEventListener( 'click', makeHandler( buttons[ i ] ) ); 109 } 110 } 111 112 /** 113 * Arrow key navigation between day cells. 114 */ 115 function initKeyboard( calendar ) { 116 var table = calendar.querySelector( '.slm-events-calendar__grid' ); 117 118 if ( ! table ) { 119 return; 120 } 121 122 var cells = table.querySelectorAll( 'td[data-slm-date]' ); 123 var i; 124 125 if ( ! cells.length ) { 126 return; 127 } 128 129 for ( i = 0; i < cells.length; i++ ) { 130 cells[ i ].setAttribute( 'tabindex', i === 0 ? '0' : '-1' ); 131 } 132 133 function focusCell( index ) { 134 if ( index < 0 || index >= cells.length ) { 135 return; 136 } 137 138 var j; 139 140 for ( j = 0; j < cells.length; j++ ) { 141 cells[ j ].setAttribute( 'tabindex', j === index ? '0' : '-1' ); 142 } 143 144 cells[ index ].focus(); 145 } 146 147 function makeHandler( index ) { 148 return function ( event ) { 149 var handled = true; 150 151 switch ( event.key ) { 152 case 'ArrowRight': 153 focusCell( index + 1 ); 154 break; 155 case 'ArrowLeft': 156 focusCell( index - 1 ); 157 break; 158 case 'ArrowDown': 159 focusCell( index + 7 ); 160 break; 161 case 'ArrowUp': 162 focusCell( index - 7 ); 163 break; 164 case 'Home': 165 focusCell( 0 ); 166 break; 167 case 'End': 168 focusCell( cells.length - 1 ); 169 break; 170 default: 171 handled = false; 172 } 173 174 if ( handled ) { 175 event.preventDefault(); 176 } 177 }; 178 } 179 180 for ( i = 0; i < cells.length; i++ ) { 181 cells[ i ].addEventListener( 'keydown', makeHandler( i ) ); 182 } 183 } 184 185 186 /** 187 * Cornerstone re-renders elements over AJAX without a page load, and its 188 * own ready event is not something to depend on. Watching the DOM for our
189 * own markup appearing is version-proof and costs nothing: the observer 190 * only does work when nodes are actually added. 191 */ 192 function watch() { 193 if ( typeof window.MutationObserver !== 'function' ) { 194 return; 195 } 196 197 var pending = null; 198 199 var observer = new window.MutationObserver( function ( mutations ) { 200 var i; 201 202 for ( i = 0; i < mutations.length; i++ ) { 203 if ( mutations[ i ].addedNodes && mutations[ i ].addedNodes.length ) { 204 // Coalesce bursts of mutations into one init pass. 205 if ( pending ) { 206 window.clearTimeout( pending ); 207 } 208 209 pending = window.setTimeout( init, 50 ); 210 211 return; 212 } 213 } 214 } ); 215 216 observer.observe( document.body || document.documentElement, { 217 childList: true, 218 subtree: true 219 } ); 220 } 221 222 /** 223 * Keep the viewport on the calendar after a month change. 224 * 225 * The navigation links deliberately carry no #fragment â Themeco Pro 226 * intercepts hash links and cancels the navigation, which stopped the 227 * month buttons working entirely. So the scroll happens here instead, 228 * only when the URL says a month was chosen. 229 */ 230 function restoreScroll( calendar ) { 231 if ( window.location.search.indexOf( 'slm_month=' ) === -1 ) { 232 return; 233 } 234 235 var reduced = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; 236 237 try { 238 calendar.scrollIntoView( { behavior: reduced ? 'auto' : 'smooth', block: 'start' } ); 239 } catch ( e ) { 240 calendar.scrollIntoView(); 241 } 242 } 243 244 var MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 245 'July', 'August', 'September', 'October', 'November', 'December' ]; 246 var AP = [ 'Jan.', 'Feb.', 'March', 'April', 'May', 'June', 247 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.' ]; 248 var DAYS = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; 249 250 function pad( n ) { 251 return n < 10 ? '0' + n : String( n ); 252 } 253 254 function readPayload( calendar ) { 255 var raw = calendar.getAttribute( 'data-slm-payload' ); 256 257 if ( ! raw ) { 258 return null; 259 } 260 261 try { 262 return JSON.parse( raw ); 263 } catch ( e ) { 264 return null; 265 } 266 } 267 268 function entryMarkup( item, hidden, overflow ) { 269 var cls = 'slm-events-calendar__entry' + ( item.o ? ' slm-events-calendar__entry--ongoing' : '' ); 270 var attrs = ' data-slm-ongoing="' + ( item.o ? '1' : '0' ) + '"'; 271 var label = ( item.t ? item.t + ' ' : '' ) + item.n; 272 var el; 273 274 if ( item.u ) { 275 el = document.createElement( 'a' ); 276 el.setAttribute( 'href', item.u ); 277 } else { 278 el = document.createElement( 'span' ); 279 } 280 281 el.className = cls; 282 el.setAttribute( 'data-slm-ongoing', item.o ? '1' : '0' ); 283 284 if ( overflow ) { 285 el.setAttribute( 'data-slm-overflow', '1' ); 286 } 287 288 el.hidden = hidden; 289 el.textContent = label; 290 291 return el; 292 } 293 294 /** 295 * Rebuild the month grid in place. 296 * 297 * Navigation is client-side because a full page load loses scroll position 298 * and closes any accordion the calendar sits inside â which makes working 299 * navigation feel broken. The links still carry real URLs, so this is an 300 * enhancement over something that works without JavaScript, not a 301 * replacement for it. 302 */ 303 function renderMonth( calendar, ym ) { 304 var payload = readPayload( calendar ); 305 var table = calendar.querySelector( '.slm-events-calendar__grid' ); 306 var agenda = calendar.querySelector( '.slm-events-calendar__agenda' ); 307 var mobile = calendar.querySelector( '.slm-events-calendar__mobile' ); 308 309 if ( ! payload || ! table ) { 310 return false; 311 } 312 313 var parts = ym.split( '-' ); 314 var year = parseInt( parts[ 0 ], 10 ); 315 var month = parseInt( parts[ 1 ], 10 ); 316 var maxShown = parseInt( calendar.getAttribute( 'data-slm-max' ) || '3', 10 ); 317 var todayKey = calendar.getAttribute( 'data-slm-today' ) || ''; 318 var hideOngoing = readPreference(); 319 320 var first = new Date( year, month - 1, 1 ); 321 var daysInMonth = new Date( year, month, 0 ).getDate(); 322 var startCol = first.getDay(); 323 324 var tbody = document.createElement( 'tbody' ); 325 var row = document.createElement( 'tr' ); 326 var i; 327 var col = 0; 328 329 for ( i = 0; i < startCol; i++ ) { 330 row.appendChild( blankCell() ); 331 col++; 332 } 333 334 for ( var day = 1; day <= daysInMonth; day++ ) { 335 var key = year + '-' + pad( month ) + '-' + pad( day ); 336 var cell = document.createElement( 'td' ); 337 338 cell.className = 'slm-events-calendar__cell' + ( key === todayKey ? ' slm-events-calendar__cell--today' : '' ); 339 cell.setAttribute( 'data-slm-date', key ); 340 341 var num = document.createElement( 'span' ); 342 num.className = 'slm-events-calendar__daynum'; 343 num.textContent = String( day ); 344 cell.appendChild( num ); 345 346 fillCell( cell, payload[ key ] || [], maxShown, hideOngoing ); 347 348 row.appendChild( cell ); 349 col++; 350 351 if ( col % 7 === 0 && day < daysInMonth ) { 352 tbody.appendChild( row ); 353 row = document.createElement( 'tr' ); 354 } 355 } 356 357 while ( col % 7 !== 0 ) { 358 row.appendChild( blankCell() ); 359 col++; 360 } 361 362 tbody.appendChild( row ); 363 364 var existing = table.querySelector( 'tbody' ); 365 366 if ( existing ) { 367 table.replaceChild( tbody, existing ); 368 } else { 369 table.appendChild( tbody ); 370 } 371 372 if ( mobile ) { 373 buildAgenda( mobile, payload, year, month, hideOngoing ); 374 } 375 376 var title = calendar.querySelector( '.slm-events-calendar__title' ); 377 378 if ( title ) { 379 title.textContent = MONTHS[ month - 1 ] + ' ' + year; 380 } 381 382 announce( calendar, MONTHS[ month - 1 ] + ' ' + year ); 383 calendar.setAttribute( 'data-slm-current', ym ); 384 385 initMore( calendar ); 386 initKeyboard( calendar ); 387 388 return true; 389 } 390 391 function blankCell() { 392 var td = document.createElement( 'td' ); 393 td.className = 'slm-events-calendar__cell slm-events-calendar__cell--empty'; 394 return td; 395 } 396 397 function fillCell( cell, items, maxShown, hideOngoing ) { 398 var shown = 0; 399 var hiddenCount = 0; 400 var i; 401 402 for ( i = 0; i < items.length; i++ ) { 403 var item = items[ i ]; 404 var isOngoing = !! item.o; 405 var overflow = false;
406 var hidden = false; 407 408 if ( isOngoing && hideOngoing ) { 409 hidden = true; 410 } else { 411 shown++; 412 413 if ( shown > maxShown ) { 414 hidden = true; 415 overflow = true; 416 hiddenCount++; 417 } 418 } 419 420 cell.appendChild( entryMarkup( item, hidden, overflow ) ); 421 } 422 423 if ( hiddenCount > 0 ) { 424 var more = document.createElement( 'button' ); 425 more.type = 'button'; 426 more.className = 'slm-events-calendar__more'; 427 more.setAttribute( 'data-slm-more', '1' ); 428 more.textContent = '+' + hiddenCount + ' more'; 429 cell.appendChild( more ); 430 } 431 } 432 433 function buildAgenda( mobile, payload, year, month, hideOngoing ) { 434 var keys = []; 435 var prefix = year + '-' + pad( month ) + '-'; 436 var key; 437 438 for ( key in payload ) { 439 if ( Object.prototype.hasOwnProperty.call( payload, key ) && key.indexOf( prefix ) === 0 ) { 440 keys.push( key ); 441 } 442 } 443 444 keys.sort(); 445 446 var wrap = document.createElement( 'div' ); 447 wrap.className = 'slm-events-calendar__agenda'; 448 449 if ( ! keys.length ) { 450 var empty = document.createElement( 'p' ); 451 empty.className = 'slm-events-empty'; 452 empty.textContent = 'Nothing on the calendar for ' + MONTHS[ month - 1 ] + ' ' + year + ' yet. Check back soon.'; 453 wrap.appendChild( empty ); 454 } 455 456 for ( var i = 0; i < keys.length; i++ ) { 457 var d = keys[ i ].split( '-' ); 458 var date = new Date( parseInt( d[ 0 ], 10 ), parseInt( d[ 1 ], 10 ) - 1, parseInt( d[ 2 ], 10 ) ); 459 460 var section = document.createElement( 'section' ); 461 section.className = 'slm-events-calendar__agenda-day'; 462 463 var h = document.createElement( 'h3' ); 464 h.className = 'slm-events-calendar__agenda-heading'; 465 h.textContent = DAYS[ date.getDay() ] + ', ' + AP[ date.getMonth() ] + ' ' + date.getDate(); 466 section.appendChild( h ); 467 468 var items = payload[ keys[ i ] ]; 469 470 for ( var j = 0; j < items.length; j++ ) { 471 section.appendChild( entryMarkup( items[ j ], items[ j ].o ? hideOngoing : false, false ) ); 472 } 473 474 wrap.appendChild( section ); 475 } 476 477 var old = mobile.querySelector( '.slm-events-calendar__agenda' ) || mobile.querySelector( '.slm-events-empty' ); 478 479 if ( old ) { 480 mobile.replaceChild( wrap, old ); 481 } else { 482 mobile.innerHTML = ''; 483 mobile.appendChild( wrap ); 484 } 485 } 486 487 function shiftMonth( ym, delta ) { 488 var parts = ym.split( '-' ); 489 var d = new Date( parseInt( parts[ 0 ], 10 ), parseInt( parts[ 1 ], 10 ) - 1 + delta, 1 ); 490 491 return d.getFullYear() + '-' + pad( d.getMonth() + 1 ); 492 } 493 494 function updateBounds( calendar ) { 495 var current = calendar.getAttribute( 'data-slm-current' ); 496 var first = calendar.getAttribute( 'data-slm-first' ); 497 var last = calendar.getAttribute( 'data-slm-last' ); 498 var prev = calendar.querySelector( '[data-slm-nav="prev"]' ); 499 var next = calendar.querySelector( '[data-slm-nav="next"]' ); 500 501 if ( prev ) { 502 prev.disabled = current <= first; 503 prev.setAttribute( 'aria-disabled', current <= first ? 'true' : 'false' ); 504 } 505 506 if ( next ) { 507 next.disabled = current >= last; 508 next.setAttribute( 'aria-disabled', current >= last ? 'true' : 'false' ); 509 } 510 } 511 512 function initNav( calendar ) { 513 var controls = calendar.querySelectorAll( '[data-slm-nav]' ); 514 var i; 515 516 function makeHandler( control ) { 517 return function ( event ) { 518 var dir = control.getAttribute( 'data-slm-nav' ); 519 var current = calendar.getAttribute( 'data-slm-current' ); 520 var target; 521 522 if ( dir === 'today' ) { 523 target = calendar.getAttribute( 'data-slm-first' ); 524 } else { 525 target = shiftMonth( current, dir === 'next' ? 1 : -1 ); 526 } 527 528 if ( target < calendar.getAttribute( 'data-slm-first' ) || target > calendar.getAttribute( 'data-slm-last' ) ) { 529 return; 530 } 531 532 // Only take over the click if the rebuild succeeds. If anything 533 // is missing, let the link navigate the old way. 534 if ( ! renderMonth( calendar, target ) ) { 535 return; 536 } 537 538 event.preventDefault(); 539 event.stopPropagation(); 540 541 updateBounds( calendar ); 542 543 if ( window.history && window.history.replaceState ) { 544 var url = window.location.pathname + '?slm_month=' + target; 545 window.history.replaceState( {}, '', url ); 546 } 547 }; 548 } 549 550 for ( i = 0; i < controls.length; i++ ) { 551 controls[ i ].addEventListener( 'click', makeHandler( controls[ i ] ) ); 552 } 553 } 554
555 function init() { 556 var calendars = document.querySelectorAll( '.slm-events-calendar' ); 557 var i; 558 559 for ( i = 0; i < calendars.length; i++ ) { 560 if ( calendars[ i ].getAttribute( 'data-slm-ready' ) === '1' ) { 561 continue; 562 } 563 564 calendars[ i ].setAttribute( 'data-slm-ready', '1' ); 565 566 // Replaces the "JavaScript has not run" line in the debug panel, 567 // so it is obvious from the page itself whether this file loaded. 568 var status = calendars[ i ].querySelector( '[data-slm-js-status]' ); 569 570 if ( status ) { 571 status.textContent = readPayload( calendars[ i ] ) 572 ? 'JavaScript active. Payload parsed.' 573 : 'JavaScript active, but the payload attribute is missing or unreadable.'; 574 } 575 576 initToggle( calendars[ i ] ); 577 initMore( calendars[ i ] ); 578 initKeyboard( calendars[ i ] ); 579 initNav( calendars[ i ] ); 580 updateBounds( calendars[ i ] ); 581 restoreScroll( calendars[ i ] ); 582 } 583 } 584 585 if ( document.readyState === 'loading' ) { 586 document.addEventListener( 'DOMContentLoaded', init ); 587 } else { 588 init(); 589 } 590 591 document.addEventListener( 'cs-ready', init ); 592 window.addEventListener( 'load', init ); 593 watch(); 594}() );
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.