Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
Strayerror (talk | contribs)
No edit summary
No edit summary
 
(43 intermediate revisions by 2 users not shown)
Line 3: Line 3:
// Wait for document ready
// Wait for document ready
$(function() {
$(function() {
var htmle = function(str) {
const htmle = (str) => String(str).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};


if ($('body').hasClass('page-Summertime_Saga_Wiki') && $('body').hasClass('action-view') && $('body').hasClass('skin-vector') && $('#ajax-posts').length && $('#ajax-version').length) {
if ($('body').hasClass('page-Summertime_Saga_Wiki') && $('body').hasClass('action-view') && $('#ajax-posts').length && $('#ajax-version').length) {
var maxPosts = 5;
const maxPosts = 5;


$.getJSON('/ssdata.json', function(data) {
$.getJSON('https://summertimesaga.com/data/wiki.php', function(data) {
$('#ajax-version').text(data.version);
$('#ajax-version').text(data.version);
$('#ajax-posts').html('');
$('#ajax-posts').html('');
Line 25: Line 23:
}());
}());


// Firefox APZ bug - Fix sticky TOC offset by forcing reposition when detected
mw.hook('wikipage.content').add(function ($content) {
mw.hook('wikipage.content').add(function ($content) {
  //const tabs = ["current", "legacy"];
if (!/firefox/i.test(navigator.userAgent) || $content.data('ffApzFixInit')) {
  const [hint, frag] = window.location.hash.slice(1).split('#');
return;
}
 
$content.data('ffApzFixInit', true);
 
const toc = document.getElementById('citizen-toc');
if (!toc) {
return;
}
 
let t = 0;
 
window.addEventListener('scroll', function() {
// Prevent multiple instances
if (t) {
return;
}
 
t = setTimeout(function () {
t = 0;
 
if (toc.getBoundingClientRect().y < 0) {
toc.classList.toggle('ff-fix');
}
}, 16);
}, { passive: true });
});
 
// Manage the Preview / Stable tabs
mw.hook('wikipage.content').add(function ($content) {
const $tabs = $content.find('ul.version-tabs');
 
// If there's no tabs, or code has already ran, return early
if (!$tabs.length || $content.data('vtInit')) {
return;
}
 
// wikipage.content can fire multiple times, safeguard against this
$content.data('vtInit', true);
 
const $toc = $('#mw-panel-toc-list');
const hasToc = $toc.length > 0;
const scrollPadding = 70; // OFfset for detecting when header should be marked 'active'
const allow = ['21', '0.20.16'];
const headingsCache = { '21': [], '0.20.16': [] };
const pref = location.hash
? (location.hash.endsWith('-0.20.16') ? '0.20.16' : '21')
: (localStorage.getItem('preferredVersion') || '21');
 
let scrollTicking = false;
let currentTab = null;
 
// Decode #fragment in case of any encoding
function decodeFragment(str) {
if (!str) {
return '';
}
 
const sliced = str.slice(1);
try { return decodeURIComponent(sliced); } catch (e) { return sliced; }
}
 
// Parse class from version tab
function tabClass(tab) {
return 'vt-' + tab.replace(/\./g, '-');
}
 
/**
* Rewrite all element IDs inside 'scopeSelector' by appending 'suffix'
* - Update internal anchor links: href="#..."
* - Update label "for" attributes: for="..."
*/
function rewriteAnchors(scopeSelector, suffix) {
const $scope = $(scopeSelector);
const idMap = {};
const baselines = {};
 
// Regex to parse base and numeric suffix
const headingIdRegex = /^(.*?)(?:_(\d+))?$/;
 
function getBaseAndIndex(id) {
const match = String(id).match(headingIdRegex);
return { base: match[1], index: match[2] ? parseInt(match[2], 10) : 1 };
}
 
// Determine baseline MediaWiki suffixes
$scope.find('[id]').each(function () {
const o = getBaseAndIndex(this.id);
if (!(o.base in baselines) || o.index < baselines[o.base]) {
baselines[o.base] = o.index;
}
});
 
// Rewrite all elements with 'id'
$scope.find('[id]').each(function () {
const $el = $(this);
const oldId = $el.attr('id');
const o = getBaseAndIndex(oldId);
const baseline = baselines[o.base];
const newIndex = o.index - baseline + 1;
const newId = (newIndex === 1) ? (o.base + suffix) : (o.base + '_' + newIndex + suffix);
 
$el.attr('id', newId);
idMap[oldId] = newId;
});
 
// Rewrite links
$scope.find('a[href^="#"]').each(function () {
const $a = $(this);
const hrefId = decodeFragment($a.attr('href'));
if (idMap[hrefId]) {
$a.attr('href', '#' + idMap[hrefId]);
}
});
 
// Rewrite labels
$scope.find('label[for]').each(function () {
const $label = $(this);
const forId = $label.attr('for');
if (idMap[forId]) {
$label.attr('for', idMap[forId]);
}
});
 
return idMap;
}
 
/**
* Apply the 'idMap' to the Citizen TOC
* - Update TOC anchors: href="#..."
* - Update Citizen's generated toc-* element ids (and sublist ids)
* - Update aria-controls on toggles
*/
function applyIdMapToToc(idMap) {
// Rewrite TOC hrefs to use new ids
$toc.find('a[href^="#"]').each(function() {
const $a = $(this);
const target = idMap[decodeFragment($a.attr('href'))];
if (target) {
$a.attr('href', '#' + target);
}
});
 
// Rewrite Citizen TOC ids (eg "toc-Heading" or "toc-Heading-sublist")
$toc.find('[id]').each(function() {
const $el = $(this);
const id = $el.attr('id');
 
if (!id || !id.startsWith('toc-')) {
return;
}
 
let key = id.slice(4); // strip "toc-"
let tail = '';
 
if (key.endsWith('-sublist')) {
key = key.slice(0, -8);
tail = '-sublist';
}
 
if (idMap[key]) {
$el.attr('id', 'toc-' + idMap[key] + tail);
}
});
 
// Rewrite aria-controls attributes
$toc.find('[aria-controls]').each(function() {
const $el = $(this);
const v = $el.attr('aria-controls');
 
if (!v || !v.startsWith('toc-')) {
return;
}
 
let key = v.slice(4);
let tail = '';
if (key.endsWith('-sublist')) {
key = key.slice(0, -8);
tail = '-sublist';
}
 
if (idMap[key]) {
$el.attr('aria-controls', 'toc-' + idMap[key] + tail);
}
});
}
 
/**
* Add data-vt-tab="..." for relevant TOC entries, based on where target element is
*/
function tagTocItemsByTarget() {
$toc.find('li[id^="toc-"]').each(function() {
const $li = $(this);
const $a = $li.find('a[href^="#"]').first();
 
if (!$a.length) {
return;
}
 
const targetId = decodeFragment($a.attr('href'));
const $target = $('#' + $.escapeSelector(targetId));
let tab = '21'; // default
 
if ($target.length) {
const $vt = $target.closest('div.vt-content');
 
if ($vt.length) {
if ($vt.hasClass(tabClass('0.20.16'))) {
tab = '0.20.16';
}
 
if ($vt.hasClass(tabClass('21'))) {
tab = '21';
}
}
}
else {
if (targetId.endsWith('-0.20.16')) {
tab = '0.20.16';
}
}
 
$li.attr('data-vt-tab', tab);
});
}
 
/**
* Hide any TOC entries that don't exist in active tab
* Uses data-vt-tab="..." set by tagTocItemsByTarget()
*/
function updateTocVisibility(activeTab) {
$toc.find('li[id^="toc-"]').each(function() {
const $li = $(this);
$li.prop('hidden', (($li.attr('data-vt-tab') || '21') !== activeTab));
});
}
 
/**
* Remove any "active" classes that Citizen adds, but keep 'citizen-toc-list-item--expanded'
*/
function clearCitizenActiveClasses() {
$toc.find('.citizen-toc-list-item--active').removeClass('citizen-toc-list-item--active');
$toc.find('.citizen-toc-level-1--active').removeClass('citizen-toc-level-1--active');
 
// Don’t remove 'citizen-toc-list-item--expanded' as it can vary due to user input
}
 
/**
* Collect heading ids from the content of 'tab'
*/
function collectHeadings(tab) {
const $container = $content.find('div.' + $.escapeSelector(tabClass(tab)));
 
if (!$container.length) {
return [];
}
 
const seen = new Set();
const items = [];
 
$container.find('.mw-headline[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
const $node = $(this);
const id = $node.attr('id');
 
if (!id || seen.has(id)) {
return;
}
 
seen.add(id);
const $headingEl = $node.closest('h1,h2,h3,h4,h5,h6');
const headingEl = $headingEl.length ? $headingEl.get(0) : $node.get(0);
const levelNum = parseInt(headingEl.tagName.toLowerCase().slice(1), 10);
 
// ignore h1 (usually page title)
if (!levelNum || levelNum <= 1) {
return;
}
 
// top is computed when tab is active/visible
items.push({ id, top: 0 });
});
 
return items;
}
 
/**
* Get 'top' positioning for all cached headings for 'tab'
* Must run only after 'tab' is visible
*/
function refreshHeadingPositions(tab) {
const list = headingsCache[tab] || [];
 
list.forEach((h) => {
const $el = $('#' + $.escapeSelector(h.id));
 
if ($el.length) {
const $headingEl = $el.closest('h1,h2,h3,h4,h5,h6');
h.top = ($headingEl.length ? $headingEl : $el).get(0).getBoundingClientRect().top + $(window).scrollTop();
return;
}
 
h.top = Number.POSITIVE_INFINITY;
});
 
list.sort((a, b) => a.top - b.top);
}
 
/**
* Calculate active heading from scroll position and update TOC
*/
function syncActiveFromScroll() {
if (!currentTab) {
return;
}
 
const list = headingsCache[currentTab];
if (!list || !list.length) {
return;
}
 
// Exdtra offset due to sticky header
const y = window.scrollY + scrollPadding;
 
// Pick the last heading top <= y.
let active = list[0];
for (let i = 0; i < list.length; i++) {
if (list[i].top <= y) {
active = list[i];
}
else {
break;
}
}
 
// Mark the TOC entry for 'headingId' as active
if (active && active.id) {
const $li = $('#toc-' + $.escapeSelector(active.id));
 
// if it doesn't exist or is hidden, skip
if (!$li.length || $li.prop('hidden')) {
return;
}
 
clearCitizenActiveClasses();
$li.addClass('citizen-toc-list-item--active');


  var pref = localStorage.getItem("preferredVersion") || "current";
// Also any parent category as active/expanded too
const $top = $li.closest('.citizen-toc-level-1');
if ($top.length) {
$top.addClass('citizen-toc-level-1--active citizen-toc-list-item--expanded');
}
}
}


  //if (hint && frag && tabs.indexOf(hint) !== -1) {
/**
  //  pref = hint;
* Activate 'tab', hide old content, show new, update tab list, set localStorage
  //  location.hash = frag;
* if the page has a TOC, update all the TOC code too
  //}
*/
function activateTab(tab) {
if (allow.indexOf(tab) === -1) {
return;
}


  function activateTab(tab) {
currentTab = tab;
    if (tabs.indexOf(tab) === -1) return;


    // Hide all tab contents
$content.find('div.vt-content').hide();
    $content.find("div.vt-content").hide();
$content.find('div.' + tabClass(tab)).show();
    // Show the selected one
$tabs.find('li').removeClass('active');
    $content.find("div.vt-" + tab).show();
$tabs.find('li[data-tab="' + tab + '"]').addClass('active');


    // Update active class on <li> tabs
localStorage.setItem('preferredVersion', tab);
    $content.find("ul.version-tabs li").removeClass("active");
    $content.find("ul.version-tabs li[data-tab='" + tab + "']").addClass("active");


    // Save selection
if (hasToc) {
    localStorage.setItem("preferredVersion", tab);
tagTocItemsByTarget();
  }
updateTocVisibility(tab);
clearCitizenActiveClasses();
refreshHeadingPositions(tab);
syncActiveFromScroll();
}
}


  // Click handler for tab <li>
// On tab click...
  $content.find("ul.version-tabs li").click(function (e) {
$tabs.on('click', 'li', function (e) {
    e.preventDefault();
e.preventDefault();
    const selected = this.dataset.tab;
activateTab(this.dataset.tab);
    activateTab(selected);
  });


  // Initialize the tab from saved preference or default
// Remove any #fragment from the old tab
  activateTab(pref);
if (window.location.hash !== '') {
history.pushState('', document.title, window.location.pathname + window.location.search);
}
});
 
const idMap02016 = rewriteAnchors('div.vt-0-20-16', '-0.20.16');
 
// If TOC enabled, update it and cache the headings
if (hasToc) {
applyIdMapToToc(idMap02016);
headingsCache['21'] = collectHeadings('21');
headingsCache['0.20.16'] = collectHeadings('0.20.16');
}
 
activateTab(pref);
 
// Add a class for styling when tabs exist
$('.citizen-body').addClass('has_version-tabs');
 
if (hasToc) {
// Update the TOC highlight in sync with scrolling, one per animation frame
window.addEventListener('scroll', function() {
if (scrollTicking) {
return;
}
 
scrollTicking = true;
 
requestAnimationFrame(() => {
scrollTicking = false;
syncActiveFromScroll();
});
}, { passive: true });
 
// Need to update positioning of headers if window is resized
window.addEventListener('resize', () => {
if (!currentTab) {
return;
}
 
refreshHeadingPositions(currentTab);
syncActiveFromScroll();
});
 
// Finish setup for TOC
tagTocItemsByTarget();
updateTocVisibility(currentTab || pref);
syncActiveFromScroll();
}
 
if (location.hash) {
location.hash = location.hash;
}
});
});

Latest revision as of 03:11, 26 January 2026

/* Any JavaScript here will be loaded for all users on every page load. */

// Wait for document ready
$(function() {
	const htmle = (str) => String(str).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));

	if ($('body').hasClass('page-Summertime_Saga_Wiki') && $('body').hasClass('action-view') && $('#ajax-posts').length && $('#ajax-version').length) {
		const maxPosts = 5;

		$.getJSON('https://summertimesaga.com/data/wiki.php', function(data) {
			$('#ajax-version').text(data.version);
			$('#ajax-posts').html('');

			data.posts.forEach(function(post, i) {
				if (i >= maxPosts) {
					return;
				}

				$('#ajax-posts').append('<dd><b>▪ ' + htmle(post.date) + ':</b> <a rel="nofollow" class="external text" href="https://www.patreon.com' + htmle(post.url) + '">' + htmle(post.title) + '</a></dd>');
			});
		});
	}
}());

// Firefox APZ bug - Fix sticky TOC offset by forcing reposition when detected
mw.hook('wikipage.content').add(function ($content) {
	if (!/firefox/i.test(navigator.userAgent) || $content.data('ffApzFixInit')) {
		return;
	}

	$content.data('ffApzFixInit', true);

	const toc = document.getElementById('citizen-toc');
	if (!toc) {
		return;
	}

	let t = 0;

	window.addEventListener('scroll', function() {
		// Prevent multiple instances
		if (t) {
			return;
		}

		t = setTimeout(function () {
			t = 0;

			if (toc.getBoundingClientRect().y < 0) {
				toc.classList.toggle('ff-fix');
			}
		}, 16);
	}, { passive: true });
});

// Manage the Preview / Stable tabs
mw.hook('wikipage.content').add(function ($content) {
	const $tabs = $content.find('ul.version-tabs');

	// If there's no tabs, or code has already ran, return early
	if (!$tabs.length || $content.data('vtInit')) {
		return;
	}

	// wikipage.content can fire multiple times, safeguard against this
	$content.data('vtInit', true);

	const $toc = $('#mw-panel-toc-list');
	const hasToc = $toc.length > 0;
	const scrollPadding = 70; // OFfset for detecting when header should be marked 'active'
	const allow = ['21', '0.20.16'];
	const headingsCache = { '21': [], '0.20.16': [] };
	const pref = location.hash
		? (location.hash.endsWith('-0.20.16') ? '0.20.16' : '21')
		: (localStorage.getItem('preferredVersion') || '21');

	let scrollTicking = false;
	let currentTab = null;

	// Decode #fragment in case of any encoding
	function decodeFragment(str) {
		if (!str) {
			return '';
		}

		const sliced = str.slice(1);
		try { return decodeURIComponent(sliced); } catch (e) { return sliced; }
	}

	// Parse class from version tab
	function tabClass(tab) {
		return 'vt-' + tab.replace(/\./g, '-');
	}

	/**
	 * Rewrite all element IDs inside 'scopeSelector' by appending 'suffix'
	 * - Update internal anchor links: href="#..."
	 * - Update label "for" attributes: for="..."
	 */
	function rewriteAnchors(scopeSelector, suffix) {
		const $scope = $(scopeSelector);
		const idMap = {};
		const baselines = {};

		// Regex to parse base and numeric suffix
		const headingIdRegex = /^(.*?)(?:_(\d+))?$/;

		function getBaseAndIndex(id) {
			const match = String(id).match(headingIdRegex);
			return { base: match[1], index: match[2] ? parseInt(match[2], 10) : 1 };
		}

		// Determine baseline MediaWiki suffixes
		$scope.find('[id]').each(function () {
			const o = getBaseAndIndex(this.id);
			if (!(o.base in baselines) || o.index < baselines[o.base]) {
				baselines[o.base] = o.index;
			}
		});

		// Rewrite all elements with 'id'
		$scope.find('[id]').each(function () {
			const $el = $(this);
			const oldId = $el.attr('id');
			const o = getBaseAndIndex(oldId);
			const baseline = baselines[o.base];
			const newIndex = o.index - baseline + 1;
			const newId = (newIndex === 1) ? (o.base + suffix) : (o.base + '_' + newIndex + suffix);

			$el.attr('id', newId);
			idMap[oldId] = newId;
		});

		// Rewrite links
		$scope.find('a[href^="#"]').each(function () {
			const $a = $(this);
			const hrefId = decodeFragment($a.attr('href'));
			if (idMap[hrefId]) {
				$a.attr('href', '#' + idMap[hrefId]);
			}
		});

		// Rewrite labels
		$scope.find('label[for]').each(function () {
			const $label = $(this);
			const forId = $label.attr('for');
			if (idMap[forId]) {
				$label.attr('for', idMap[forId]);
			}
		});

		return idMap;
	}

	/**
	 * Apply the 'idMap' to the Citizen TOC
	 * - Update TOC anchors: href="#..."
	 * - Update Citizen's generated toc-* element ids (and sublist ids)
	 * - Update aria-controls on toggles
	 */
	function applyIdMapToToc(idMap) {
		// Rewrite TOC hrefs to use new ids
		$toc.find('a[href^="#"]').each(function() {
			const $a = $(this);
			const target = idMap[decodeFragment($a.attr('href'))];
			if (target) {
				$a.attr('href', '#' + target);
			}
		});

		// Rewrite Citizen TOC ids (eg "toc-Heading" or "toc-Heading-sublist")
		$toc.find('[id]').each(function() {
			const $el = $(this);
			const id = $el.attr('id');

			if (!id || !id.startsWith('toc-')) {
				return;
			}

			let key = id.slice(4); // strip "toc-"
			let tail = '';

			if (key.endsWith('-sublist')) {
				key = key.slice(0, -8);
				tail = '-sublist';
			}

			if (idMap[key]) {
				$el.attr('id', 'toc-' + idMap[key] + tail);
			}
		});

		// Rewrite aria-controls attributes
		$toc.find('[aria-controls]').each(function() {
			const $el = $(this);
			const v = $el.attr('aria-controls');

			if (!v || !v.startsWith('toc-')) {
				return;
			}

			let key = v.slice(4);
			let tail = '';
			if (key.endsWith('-sublist')) {
				key = key.slice(0, -8);
				tail = '-sublist';
			}

			if (idMap[key]) {
				$el.attr('aria-controls', 'toc-' + idMap[key] + tail);
			}
		});
	}

	/**
	 * Add data-vt-tab="..." for relevant TOC entries, based on where target element is
	 */
	function tagTocItemsByTarget() {
		$toc.find('li[id^="toc-"]').each(function() {
			const $li = $(this);
			const $a = $li.find('a[href^="#"]').first();

			if (!$a.length) {
				return;
			}

			const targetId = decodeFragment($a.attr('href'));
			const $target = $('#' + $.escapeSelector(targetId));
			let tab = '21'; // default

			if ($target.length) {
				const $vt = $target.closest('div.vt-content');

				if ($vt.length) {
					if ($vt.hasClass(tabClass('0.20.16'))) {
						tab = '0.20.16';
					}

					if ($vt.hasClass(tabClass('21'))) {
						tab = '21';
					}
				}
			}
			else {
				if (targetId.endsWith('-0.20.16')) {
					tab = '0.20.16';
				}
			}

			$li.attr('data-vt-tab', tab);
		});
	}

	/**
	 * Hide any TOC entries that don't exist in active tab
	 * Uses data-vt-tab="..." set by tagTocItemsByTarget()
	 */
	function updateTocVisibility(activeTab) {
		$toc.find('li[id^="toc-"]').each(function() {
			const $li = $(this);
			$li.prop('hidden', (($li.attr('data-vt-tab') || '21') !== activeTab));
		});
	}

	/**
	 * Remove any "active" classes that Citizen adds, but keep 'citizen-toc-list-item--expanded'
	 */
	function clearCitizenActiveClasses() {
		$toc.find('.citizen-toc-list-item--active').removeClass('citizen-toc-list-item--active');
		$toc.find('.citizen-toc-level-1--active').removeClass('citizen-toc-level-1--active');

		// Don’t remove 'citizen-toc-list-item--expanded' as it can vary due to user input
	}

	/**
	 * Collect heading ids from the content of 'tab'
	 */
	function collectHeadings(tab) {
		const $container = $content.find('div.' + $.escapeSelector(tabClass(tab)));

		if (!$container.length) {
			return [];
		}

		const seen = new Set();
		const items = [];

		$container.find('.mw-headline[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
			const $node = $(this);
			const id = $node.attr('id');

			if (!id || seen.has(id)) {
				return;
			}

			seen.add(id);
			const $headingEl = $node.closest('h1,h2,h3,h4,h5,h6');
			const headingEl = $headingEl.length ? $headingEl.get(0) : $node.get(0);
			const levelNum = parseInt(headingEl.tagName.toLowerCase().slice(1), 10);

			// ignore h1 (usually page title)
			if (!levelNum || levelNum <= 1) {
				return;
			}

			// top is computed when tab is active/visible
			items.push({ id, top: 0 });
		});

		return items;
	}

	/**
	 * Get 'top' positioning for all cached headings for 'tab'
	 * Must run only after 'tab' is visible
	 */
	function refreshHeadingPositions(tab) {
		const list = headingsCache[tab] || [];

		list.forEach((h) => {
			const $el = $('#' + $.escapeSelector(h.id));

			if ($el.length) {
				const $headingEl = $el.closest('h1,h2,h3,h4,h5,h6');
				h.top = ($headingEl.length ? $headingEl : $el).get(0).getBoundingClientRect().top + $(window).scrollTop();
				return;
			}

			h.top = Number.POSITIVE_INFINITY;
		});

		list.sort((a, b) => a.top - b.top);
	}

	/**
	 * Calculate active heading from scroll position and update TOC
	 */
	function syncActiveFromScroll() {
		if (!currentTab) {
			return;
		}

		const list = headingsCache[currentTab];
		if (!list || !list.length) {
			return;
		}

		// Exdtra offset due to sticky header
		const y = window.scrollY + scrollPadding;

		// Pick the last heading top <= y.
		let active = list[0];
		for (let i = 0; i < list.length; i++) {
			if (list[i].top <= y) {
				active = list[i];
			}
			else {
				break;
			}
		}

		// Mark the TOC entry for 'headingId' as active
		if (active && active.id) {
			const $li = $('#toc-' + $.escapeSelector(active.id));

			// if it doesn't exist or is hidden, skip
			if (!$li.length || $li.prop('hidden')) {
				return;
			}

			clearCitizenActiveClasses();
			$li.addClass('citizen-toc-list-item--active');

			// Also any parent category as active/expanded too
			const $top = $li.closest('.citizen-toc-level-1');
			if ($top.length) {
				$top.addClass('citizen-toc-level-1--active citizen-toc-list-item--expanded');
			}
		}
	}

	/**
	 * Activate 'tab', hide old content, show new, update tab list, set localStorage
	 * if the page has a TOC, update all the TOC code too
	 */
	function activateTab(tab) {
		if (allow.indexOf(tab) === -1) {
			return;
		}

		currentTab = tab;

		$content.find('div.vt-content').hide();
		$content.find('div.' + tabClass(tab)).show();
		$tabs.find('li').removeClass('active');
		$tabs.find('li[data-tab="' + tab + '"]').addClass('active');

		localStorage.setItem('preferredVersion', tab);

		if (hasToc) {
			tagTocItemsByTarget();
			updateTocVisibility(tab);
			clearCitizenActiveClasses();
			refreshHeadingPositions(tab);
			syncActiveFromScroll();
		}
	}

	// On tab click...
	$tabs.on('click', 'li', function (e) {
		e.preventDefault();
		activateTab(this.dataset.tab);

		// Remove any #fragment from the old tab
		if (window.location.hash !== '') {
			history.pushState('', document.title, window.location.pathname + window.location.search);
		}
	});

	const idMap02016 = rewriteAnchors('div.vt-0-20-16', '-0.20.16');

	// If TOC enabled, update it and cache the headings
	if (hasToc) {
		applyIdMapToToc(idMap02016);
		headingsCache['21'] = collectHeadings('21');
		headingsCache['0.20.16'] = collectHeadings('0.20.16');
	}

	activateTab(pref);

	// Add a class for styling when tabs exist
	$('.citizen-body').addClass('has_version-tabs');

	if (hasToc) {
		// Update the TOC highlight in sync with scrolling, one per animation frame
		window.addEventListener('scroll', function() {
			if (scrollTicking) {
				return;
			}

			scrollTicking = true;

			requestAnimationFrame(() => {
				scrollTicking = false;
				syncActiveFromScroll();
			});
		}, { passive: true });

		// Need to update positioning of headers if window is resized
		window.addEventListener('resize', () => {
			if (!currentTab) {
				return;
			}

			refreshHeadingPositions(currentTab);
			syncActiveFromScroll();
		});

		// Finish setup for TOC
		tagTocItemsByTarget();
		updateTocVisibility(currentTab || pref);
		syncActiveFromScroll();
	}

	if (location.hash) {
		location.hash = location.hash;
	}
});