MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
Strayerror (talk | contribs) No edit summary |
No edit summary |
||
| (39 intermediate revisions by 2 users not shown) | |||
| Line 3: | Line 3: | ||
// Wait for document ready | // Wait for document ready | ||
$(function() { | $(function() { | ||
const htmle = (str) => String(str).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | |||
if ($('body').hasClass('page-Summertime_Saga_Wiki') && $('body').hasClass('action-view | if ($('body').hasClass('page-Summertime_Saga_Wiki') && $('body').hasClass('action-view') && $('#ajax-posts').length && $('#ajax-version').length) { | ||
const maxPosts = 5; | |||
$.getJSON('/ | $.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) { | ||
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; | |||
} | |||
}); | }); | ||
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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;
}
});