PageSourceSearch

https://salem4u.com/wp-content/plugins/salem-events/assets/js/salem-events-calendar.js?ver=1.6.2.1788449853

js salem4u.com collected 2026-10-02 06:34:58 UTC 15,666 bytes, 594 lines download raw bytes

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.