MediaWiki:Common.js: Difference between revisions
Appearance
Yadasampati (talk | contribs) Blanked the page Tags: Blanking Manual revert |
No edit summary |
||
| Line 1: | Line 1: | ||
/* Any JavaScript here will be loaded for all users on every page load. */ | |||
if (mw.config.get('wgPageName' ) === 'Krishna_says_in_Bhagavad-gita') { | |||
/* switch tooltip texts from content-div to label-div */ | |||
const v_labels = Array.from(document.getElementsByClassName("tabs-label")); | |||
const v_content = Array.from(document.getElementsByClassName("tabs-content")); | |||
for (var i = 0; i < v_labels.length; i++) { | |||
v_labels[i].title = v_content[i].title; | |||
v_content[i].title = ""; | |||
} | |||
/* adjust wikitables */ | |||
var arr = Array.from(document.getElementsByClassName("wikitable")); | |||
for (var t = 0; t < arr.length; t++) { | |||
/* insert THEAD in wikitable */ | |||
var thead = document.createElement("thead"); | |||
thead.style.display = "block"; | |||
var wtbl = arr[t]; | |||
wtbl.insertBefore(thead, wtbl.firstChild); | |||
/* move first row of TBODY to THEAD */ | |||
thead.appendChild(wtbl.rows[0]); | |||
/* make TBODY scrollable */ | |||
var tbody = wtbl.tBodies[0]; | |||
tbody.style.display = "block"; | |||
tbody.style.overflowY = "auto"; | |||
tbody.style.maxHeight = "500px"; | |||
for (var r = 0; r < wtbl.rows.length; r++) { | |||
var row = wtbl.rows[r]; | |||
/* adjust column widths and hide from-until columns */ | |||
row.cells[0].classList = "cw_0"; | |||
row.cells[1].classList = "cw_150"; | |||
row.cells[2].classList = "cw_500"; | |||
/* mark the text */ | |||
if (r == 0) continue; | |||
var v_text = row.cells[2].textContent; | |||
var v_html = ""; var v_from = 0; | |||
var marks = JSON.parse(row.cells[0].textContent.replaceAll("O", "[").replaceAll("C", "]")); | |||
for (var m = 0; m < marks.length; m++) { | |||
v_html += | |||
v_text.substr(v_from,marks[m][0]-v_from) + "<mark>" + | |||
v_text.substr(marks[m][0],marks[m][1]-marks[m][0]+1) + "</mark>"; | |||
v_from = marks[m][1]+1; | |||
} | |||
v_html += v_text.substr(v_from); | |||
row.cells[2].innerHTML = v_html; | |||
} | |||
} | |||
} | |||
// install Google Search | |||
// document.write('<script type="text/javascript" src="' | |||
// + 'http://vanipedia.org/w/index.php?title=MediaWiki:GoogleSearch.js' | |||
// + '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install JQuery | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:JQuery.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install WikEd toolbar and Vanify | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vaniquotes.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install Replace | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:TextboxReplace.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install Unify | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Unify.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install Vanify | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vanify.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install Vani_suggest for diacritics auto-suggest pulldown on the search bar | |||
//document.write('<script type="text/javascript" src="' | |||
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vani_suggest.js' | |||
//+ '&action=raw&ctype=text/javascript"></' + 'script>'); | |||
// install previous/next links for video categories in all languages | |||
//document.write('<script type="text/javascript" src="/navcat/navcat.js"></' + 'script>'); | |||
// Buttons | |||
//addOnloadHook(function() { | |||
//if (document.editform) { | |||
//addPortletLink("p-cactions", "javascript:Unify()", "unify", "ca-unify", "Convert Balarama fonts to Unicode", ""); | |||
// addPortletLink("p-cactions", "javascript:Vanify()", "vanify", "ca-vanify", "Corrects vani markup", ""); | |||
// addPortletLink('p-cactions', 'javascript:format()', 'format', 'ca-format', 'Format article', ''); | |||
//addPortletLink('p-cactions', 'javascript:wpTextboxReplace()', 'Replace', 'ca-replace', 'Replaces text in the edit window', 'R'); | |||
//} | |||
//}); | |||
// end Buttons | |||
// --- YouTube Random Shorts (Kasya) --- | |||
$(document).ready(function() { | |||
var $widget = $('.vani-shorts-viewport'); | |||
if ($widget.length) { | |||
var $viewport = $widget; | |||
var $titleFrame = $widget.find('.vani-shorts-title'); | |||
new mw.Api().get({ | |||
action: 'query', | |||
prop: 'revisions', | |||
titles: 'VanimediaMayapur YouTube SHORTS List', | |||
rvprop: 'content', | |||
format: 'json' | |||
}).done(function(data) { | |||
try { | |||
var pages = data.query.pages; | |||
var pageId = Object.keys(pages)[0]; | |||
if (pageId == "-1" || !pages[pageId].revisions) { | |||
$viewport.text("Database list page not found."); | |||
return; | |||
} | |||
// FIXED: Corrected MediaWiki revision array syntax lookup | |||
var rawWikiText = pages[pageId].revisions[0]['*']; | |||
var lines = rawWikiText.split('\n'); | |||
var validShorts = []; | |||
for (var i = 0; i < lines.length; i++) { | |||
var line = $.trim(lines[i]); | |||
if (line.indexOf('File:') === 0 && line.toLowerCase().indexOf('link=') !== -1) { | |||
validShorts.push(line); | |||
} | |||
} | |||
if (validShorts.length === 0) { | |||
$viewport.text("No gallery entries recognized."); | |||
return; | |||
} | |||
var randomLine = validShorts[Math.floor(Math.random() * validShorts.length)]; | |||
var parts = randomLine.split('|'); | |||
// Reset defaults before parsing line elements | |||
var rawUrl = ""; | |||
for (var j = 0; j < parts.length; j++) { | |||
var part = $.trim(parts[j]); | |||
if (part.toLowerCase().indexOf('link=') === 0) { | |||
rawUrl = part.substring(5).trim(); | |||
} | |||
} | |||
// Clean all template formatting brackets, space characters, and hidden control markers | |||
var urlCleaned = rawUrl.replace(/[\u200e\u200f\s\}\}\]]/g, ''); | |||
// Direct YouTube Alphanumeric Pattern Matcher | |||
var idRegex = /(?:shorts\/|v=|embed\/|\/)([a-zA-Z0-9_-]{11})/; | |||
var matchResults = urlCleaned.match(idRegex); | |||
var videoId = ""; | |||
if (matchResults && matchResults[1]) { | |||
// FIXED: Grabbed index 1 to pull the standalone 11-digit ID without any slashes | |||
videoId = matchResults[1].trim(); | |||
} | |||
if (videoId && videoId.length === 11) { | |||
// FIXED: Isolated to inject ONLY the video player iframe at the correct 280px width | |||
var embedUrl = "https://www.youtube.com/embed/" + videoId; | |||
$viewport.html('<iframe width="280" height="225" src="' + embedUrl + '" frameborder="0" allowfullscreen style="border-radius: 8px; border: none;"></iframe>'); | |||
} else { | |||
$viewport.html("<div style='color:#c0392b; padding:10px; font-size:11px;'>Invalid video ID sequence extracted.</div>"); | |||
} | |||
} catch (err) { | |||
console.error("Shorts engine exception:", err); | |||
$viewport.text("Error handling text structure values."); | |||
} | |||
}).fail(function() { | |||
$viewport.text("API connection failed."); | |||
}); | |||
} | |||
}); | |||
// Kasya | |||
/* Open Timeline links in a new tab ONLY on the Chapters page */ | |||
mw.hook('wikipage.content').add(function($content) { | |||
// Check if we are exactly on the timeline page | |||
if (mw.config.get('wgPageName') === "Chapters_in_Srila_Prabhupada's_Life") { | |||
// If yes, apply the new tab rule to the table | |||
$content.find('#mp-tfp a').attr('target', '_blank').attr('rel', 'noopener noreferrer'); | |||
} | |||
}); | |||
// Wait for MediaWiki core modules and the DOM to load | |||
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { | |||
$(document).ready(function () { | |||
// 1. FIND THE "102 Languages" SIDEBAR SECTION | |||
var $sidebarMenu = $('#p-102_Languages ul').first(); | |||
// Fallback in case your specific MediaWiki version formats the ID differently | |||
if ($sidebarMenu.length === 0) { | |||
$('.vector-menu-heading, .portal h3').each(function() { | |||
// Looks for the heading text directly | |||
if ($(this).text().trim() === '102 Languages') { | |||
$sidebarMenu = $(this).closest('.vector-menu, .portal').find('ul').first(); | |||
} | |||
}); | |||
} | |||
// Exit if the 102 Languages menu isn't found on the page | |||
if (!$sidebarMenu || $sidebarMenu.length === 0) return; | |||
// 2. CREATE THE SEARCH BAR AS A MENU ITEM | |||
var $searchLi = $('<li id="n-language-search" style="margin-top: 10px; margin-bottom: 5px; list-style: none;"></li>'); | |||
var $searchBox = $('<input type="text" id="sidebarLanguageSearch" placeholder="Search 102 Languages..." style="width: 100%; box-sizing: border-box; padding: 5px; font-size: 0.85em; border: 1px solid #a2a9b1; border-radius: 2px;" autocomplete="off" />'); | |||
// Container for the results (hidden by default) | |||
var $resultsContainer = $('<ul id="sidebarLanguageResults" style="list-style: none; padding: 0; margin: 4px 0 0 0; max-height: 200px; overflow-y: auto; border: 1px solid #eaecf0; background: #ffffff; display: none;"></ul>'); | |||
// Build the menu item and append it to the sidebar | |||
$searchLi.append($searchBox).append($resultsContainer); | |||
$sidebarMenu.prepend($searchLi); | |||
var api = new mw.Api(); | |||
var languageList = []; | |||
// 3. FETCH LANGUAGES FROM THE CATEGORY VIA API IN THE BACKGROUND | |||
api.get({ | |||
action: 'query', | |||
list: 'categorymembers', | |||
cmtitle: 'Category:Languages_with_Total_Pages', | |||
cmlimit: 'max', | |||
format: 'json' | |||
}).done(function (data) { | |||
if (data.query && data.query.categorymembers) { | |||
languageList = data.query.categorymembers.map(function (member) { | |||
return { | |||
title: member.title, | |||
url: mw.util.getUrl(member.title) | |||
}; | |||
}); | |||
} | |||
}); | |||
// 4. ADD SEARCH FUNCTIONALITY TO THE SIDEBAR BOX | |||
$('#sidebarLanguageSearch').on('keyup', function (e) { | |||
e.stopPropagation(); | |||
var searchTerm = $(this).val().toLowerCase(); | |||
$resultsContainer.empty(); | |||
// Hide the results box if the search is empty | |||
if (searchTerm.length === 0) { | |||
$resultsContainer.hide(); | |||
return; | |||
} | |||
// Filter the fetched languages | |||
var filteredLanguages = languageList.filter(function (lang) { | |||
return lang.title.toLowerCase().indexOf(searchTerm) > -1; | |||
}); | |||
// Show results container | |||
$resultsContainer.show(); | |||
// Append results using Vector's standard dark text color | |||
if (filteredLanguages.length > 0) { | |||
filteredLanguages.forEach(function (lang) { | |||
$resultsContainer.append('<li style="padding: 4px 6px; border-bottom: 1px solid #f8f9fa;"><a href="' + lang.url + '" style="text-decoration: none; color: #0645ad; display: block; font-size: 0.9em;">' + lang.title + '</a></li>'); | |||
}); | |||
} else { | |||
$resultsContainer.append('<li style="padding: 4px 6px; color: #72777d; font-size: 0.9em;">No languages found</li>'); | |||
} | |||
}); | |||
// Prevent typing from accidentally triggering global keyboard shortcuts | |||
$('#sidebarLanguageSearch').on('keydown', function(e) { | |||
e.stopPropagation(); | |||
}); | |||
// Optional: Hide results when clicking outside the sidebar search | |||
$(document).on('click', function(e) { | |||
if (!$(e.target).closest('#n-language-search').length) { | |||
$resultsContainer.hide(); | |||
} | |||
}); | |||
// Re-show results if they click back into the search box | |||
$('#sidebarLanguageSearch').on('click', function() { | |||
if ($(this).val().length > 0) { | |||
$resultsContainer.show(); | |||
} | |||
}); | |||
}); | |||
}); | |||
// 5. SIDEBAR ACCORDION - STRICT LEFT-COLUMN ISOLATION & TRUE ACCORDION | |||
(function() { | |||
// 1. Tag ONLY menus physically located in the left sidebar | |||
function tagSidebarMenus() { | |||
var allMenus = document.querySelectorAll('.vector-menu'); | |||
for (var i = 0; i < allMenus.length; i++) { | |||
var menu = allMenus[i]; | |||
// STRICT INCLUSION: Only tag menus inside the modern or legacy left sidebar containers | |||
if (menu.closest('#vector-main-menu, #mw-panel, #mw-navigation')) { | |||
menu.classList.add('vanipedia-sidebar-menu'); | |||
// Collapse them by default if they have a heading | |||
if (menu.querySelector('.vector-menu-heading') && !menu.classList.contains('vanipedia-collapsed')) { | |||
menu.classList.add('vanipedia-collapsed'); | |||
} | |||
} | |||
} | |||
} | |||
// Run tagging immediately, and again on load as a failsafe | |||
tagSidebarMenus(); | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', tagSidebarMenus); | |||
} | |||
// 2. Inject CSS targeted STRICTLY at our newly tagged custom class | |||
var style = document.createElement('style'); | |||
style.textContent = | |||
/* Widen only our tagged sidebar menus */ | |||
'.vanipedia-sidebar-menu { width: 185px !important; } ' + | |||
/* Core functional hiding rule */ | |||
'.vanipedia-sidebar-menu.vanipedia-collapsed .vector-menu-content { display: none !important; } ' + | |||
/* Bold, Dark Header */ | |||
'.vanipedia-sidebar-menu .vector-menu-heading { ' + | |||
' cursor: pointer !important; ' + | |||
' background-color: #2c4c50 !important; ' + | |||
' border: 1px solid #1b3033 !important; ' + | |||
' border-radius: 4px !important; ' + | |||
' margin-bottom: 4px !important; ' + | |||
' padding: 10px 12px !important; ' + | |||
' font-weight: 600 !important; ' + | |||
' color: #ffffff !important; ' + | |||
' display: flex !important; ' + | |||
' justify-content: space-between !important; ' + | |||
' align-items: center !important; ' + | |||
' gap: 10px !important; ' + | |||
' transition: all 0.2s ease; ' + | |||
'} ' + | |||
/* Fix header corners when expanded */ | |||
'.vanipedia-sidebar-menu:not(.vanipedia-collapsed) .vector-menu-heading { ' + | |||
' border-radius: 4px 4px 0 0 !important; ' + | |||
' margin-bottom: 0 !important; ' + | |||
'} ' + | |||
/* Hover effect */ | |||
'.vanipedia-sidebar-menu .vector-menu-heading:hover { background-color: #385e63 !important; } ' + | |||
/* The dropdown caret */ | |||
'.vanipedia-sidebar-menu .vector-menu-heading::after { ' + | |||
' content: "▼"; ' + | |||
' font-size: 0.75em; ' + | |||
' color: #ffffff !important; ' + | |||
' transition: transform 0.2s ease; ' + | |||
'} ' + | |||
/* Rotate the arrow when collapsed */ | |||
'.vanipedia-sidebar-menu.vanipedia-collapsed .vector-menu-heading::after { ' + | |||
' transform: rotate(-90deg); ' + | |||
'} ' + | |||
/* The Expanded Content Area */ | |||
'.vanipedia-sidebar-menu .vector-menu-content { ' + | |||
' background-color: #eaeff0 !important; ' + | |||
' border: 1px solid #2c4c50 !important; ' + | |||
' border-top: none !important; ' + | |||
' border-radius: 0 0 4px 4px !important; ' + | |||
' padding: 0 0 4px 0 !important; ' + | |||
' margin-bottom: 6px !important; ' + | |||
' box-shadow: 0 3px 6px rgba(0,0,0,0.15) !important; ' + | |||
'} ' + | |||
/* Reset the list styles */ | |||
'.vanipedia-sidebar-menu .vector-menu-content ul { ' + | |||
' list-style: none !important; ' + | |||
' margin: 0 !important; ' + | |||
' padding: 0 !important; ' + | |||
'} ' + | |||
/* Menu links */ | |||
'.vanipedia-sidebar-menu .vector-menu-content li a { ' + | |||
' display: block !important; ' + | |||
' padding: 8px 12px 8px 16px !important; ' + | |||
' color: #1b3033 !important; ' + | |||
' text-decoration: none !important; ' + | |||
' border-left: 3px solid transparent !important; ' + | |||
' border-bottom: 1px solid rgba(44, 76, 80, 0.1) !important; ' + | |||
' transition: all 0.15s ease !important; ' + | |||
'} ' + | |||
/* Remove separator from last item */ | |||
'.vanipedia-sidebar-menu .vector-menu-content li:last-child a { ' + | |||
' border-bottom: none !important; ' + | |||
'} ' + | |||
/* Hover effect for individual menu items */ | |||
'.vanipedia-sidebar-menu .vector-menu-content li a:hover { ' + | |||
' background-color: #dbe4e6 !important; ' + | |||
' border-left: 3px solid #2c4c50 !important; ' + | |||
' color: #000000 !important; ' + | |||
'} '; | |||
document.head.appendChild(style); | |||
// 3. Helper function to collapse all of our securely tagged menus | |||
function collapseAllSidebarMenus() { | |||
var menus = document.querySelectorAll('.vanipedia-sidebar-menu'); | |||
for (var i = 0; i < menus.length; i++) { | |||
menus[i].classList.add('vanipedia-collapsed'); | |||
} | |||
} | |||
// 4. Intercept clicks for True Accordion Behavior | |||
document.addEventListener('click', function(e) { | |||
var heading = e.target.closest('.vector-menu-heading'); | |||
if (!heading) return; | |||
// ONLY react if this click belongs to one of our tagged sidebar menus | |||
var navContainer = heading.closest('.vanipedia-sidebar-menu'); | |||
if (navContainer) { | |||
var isAlreadyOpen = !navContainer.classList.contains('vanipedia-collapsed'); | |||
// Forcefully collapse ALL left sidebar menus first | |||
collapseAllSidebarMenus(); | |||
// Open the clicked one if it wasn't already open | |||
if (!isAlreadyOpen) { | |||
navContainer.classList.remove('vanipedia-collapsed'); | |||
} | |||
e.stopPropagation(); | |||
e.preventDefault(); | |||
} | |||
}, true); | |||
})(); | |||
mw.loader.using('mediawiki.user').then(function() { | |||
// Fetch the user's assigned groups | |||
var userGroups = mw.config.get('wgUserGroups') || []; | |||
// Define the roles that SHOULD see the administrative tabs | |||
var isAuthorized = userGroups.includes('sysop') || | |||
userGroups.includes('interface-admin') || | |||
userGroups.includes('Editor'); | |||
// If the user is not in one of those groups, restrict the UI | |||
if (!isAuthorized) { | |||
document.body.classList.add('restricted-ui'); | |||
} | |||
}); | |||
// Function to open accordion from TOC and jump there // | |||
mw.hook('wikipage.content').add(function() { | |||
function handleAccordionTOC(hash) { | |||
if (!hash || hash === '#') return false; | |||
try { | |||
var targetId = hash.substring(1); | |||
var targetElement = document.getElementById(targetId) || document.getElementById(decodeURIComponent(targetId)); | |||
if (!targetElement) return false; | |||
var $target = $(targetElement); | |||
// Match either h2 or h3 | |||
var $heading = $target.is('h2, h3') ? $target : $target.closest('h2, h3'); | |||
if (!$heading.length) return false; | |||
var $accordion = $heading.next('.book-accordion'); | |||
// If the heading itself is not followed directly by an accordion, | |||
// check if the heading is located INSIDE a parent accordion (e.g., nested h3) | |||
var $parentAccordions = $heading.parents('.book-accordion'); | |||
// If there's no direct accordion next to it and it's not inside any accordion, | |||
// let the browser handle standard jump. | |||
if (!$accordion.length && !$parentAccordions.length) return false; | |||
// 1. Open all ancestor accordions (if target heading is nested inside one) | |||
$parentAccordions.each(function() { | |||
var $parentCollapsible = $(this).children('.mw-collapsible'); | |||
if ($parentCollapsible.length && $parentCollapsible.hasClass('mw-collapsed')) { | |||
var pId = $parentCollapsible.attr('id'); | |||
if (pId) { | |||
var pToggleClass = pId.replace('mw-customcollapsible-', 'mw-customtoggle-'); | |||
$('.' + pToggleClass).first().click(); | |||
} | |||
} | |||
}); | |||
// 2. Open the accordion immediately following this heading (if present) | |||
if ($accordion.length) { | |||
var $collapsible = $accordion.children('.mw-collapsible, .mw-collapsible-content').first(); | |||
if (!$collapsible.length) { | |||
$collapsible = $accordion.find('.mw-collapsible'); | |||
} | |||
if ($collapsible.length && $collapsible.hasClass('mw-collapsed')) { | |||
var collapsibleId = $collapsible.attr('id'); | |||
if (collapsibleId) { | |||
var toggleClass = collapsibleId.replace('mw-customcollapsible-', 'mw-customtoggle-'); | |||
$('.' + toggleClass).first().click(); | |||
} | |||
} | |||
} | |||
// Target element to scroll to (the accordion, or the heading itself if accordion is missing) | |||
var $scrollTarget = $accordion.length ? $accordion : $heading; | |||
// 3. Scroll to the visible section | |||
setTimeout(function() { | |||
$('html, body').animate({ | |||
scrollTop: $scrollTarget.offset().top - 75 | |||
}, 300); | |||
}, 100); | |||
// Return true to indicate we successfully handled the TOC accordion click | |||
return true; | |||
} catch (e) { | |||
console.error("TOC Accordion script error:", e); | |||
return false; | |||
} | |||
} | |||
$(document).off('click.tocAccordion').on('click.tocAccordion', 'a[href^="#"]', function(e) { | |||
if ($(this).closest('#toc, .toc, .vector-toc').length > 0) { | |||
var href = $(this).attr('href'); | |||
// Run our function to see if this is an accordion/heading link | |||
var isAccordion = handleAccordionTOC(href); | |||
// ONLY stop the default browser jump if we handled it | |||
if (isAccordion) { | |||
e.preventDefault(); | |||
// Update the URL manually so the user can still share the link | |||
if (history.pushState) { | |||
history.pushState(null, null, href); | |||
} else { | |||
window.location.hash = href; | |||
} | |||
} | |||
} | |||
}); | |||
if (window.location.hash) { | |||
setTimeout(function() { | |||
handleAccordionTOC(window.location.hash); | |||
}, 500); | |||
} | |||
}); | |||
/* Sticky burger menu - Pure Floating Dropdown (Forces Unpinned & Full Title) */ | |||
(function () { | |||
let isMenuOpen = false; | |||
// 1. Force the sidebar out of pinned mode and return menu into the dropdown container | |||
function forceUnpinSidebar() { | |||
document.documentElement.classList.remove('vector-feature-main-menu-pinned-enabled', 'vector-feature-main-menu-pinned-clientpref-1'); | |||
document.documentElement.classList.add('vector-feature-main-menu-pinned-disabled', 'vector-feature-main-menu-pinned-clientpref-0'); | |||
if (document.body) { | |||
document.body.classList.remove('vector-feature-main-menu-pinned-enabled', 'vector-feature-main-menu-pinned-clientpref-1'); | |||
document.body.classList.add('vector-feature-main-menu-pinned-disabled', 'vector-feature-main-menu-pinned-clientpref-0'); | |||
} | |||
// Clear stored pin preference | |||
document.cookie = "vector-feature-main-menu-pinned=0; path=/; max-age=31536000"; | |||
try { localStorage.setItem('vector-feature-main-menu-pinned', '0'); } catch(e) {} | |||
// If #vector-main-menu was moved into the pinned container, move it back into dropdown | |||
const mainMenu = document.getElementById('vector-main-menu'); | |||
const unpinnedContainer = document.getElementById('vector-main-menu-unpinned-container') || | |||
document.querySelector('#vector-main-menu-dropdown .vector-dropdown-content'); | |||
const pinnedContainer = document.getElementById('vector-main-menu-pinned-container'); | |||
if (mainMenu && unpinnedContainer && pinnedContainer && pinnedContainer.contains(mainMenu)) { | |||
unpinnedContainer.appendChild(mainMenu); | |||
} | |||
} | |||
function getDropdownElement() { | |||
return document.querySelector('#vector-main-menu-dropdown .vector-dropdown-content') || | |||
document.getElementById('vector-main-menu-dropdown-content') || | |||
document.getElementById('vector-main-menu-unpinned-container') || | |||
document.getElementById('vector-main-menu'); | |||
} | |||
function openMenu(menuBtn) { | |||
forceUnpinSidebar(); | |||
const checkbox = document.getElementById('vector-main-menu-dropdown-checkbox'); | |||
const dropdown = getDropdownElement(); | |||
if (!dropdown) return; | |||
if (checkbox) { | |||
checkbox.checked = true; | |||
checkbox.dispatchEvent(new Event('change', { bubbles: true })); | |||
} | |||
const rect = menuBtn.getBoundingClientRect(); | |||
dropdown.style.setProperty('position', 'fixed', 'important'); | |||
dropdown.style.setProperty('top', rect.bottom + 'px', 'important'); | |||
dropdown.style.setProperty('left', rect.left + 'px', 'important'); | |||
dropdown.style.setProperty('z-index', '99999', 'important'); | |||
dropdown.style.setProperty('max-height', (window.innerHeight - rect.bottom - 16) + 'px', 'important'); | |||
dropdown.style.setProperty('overflow-y', 'auto', 'important'); | |||
dropdown.style.setProperty('display', 'block', 'important'); | |||
dropdown.style.setProperty('visibility', 'visible', 'important'); | |||
dropdown.style.setProperty('opacity', '1', 'important'); | |||
dropdown.style.setProperty('background-color', 'var(--background-color-base, #ffffff)', 'important'); | |||
dropdown.style.setProperty('box-shadow', '0 4px 16px rgba(0, 0, 0, 0.2)', 'important'); | |||
dropdown.style.setProperty('border-radius', '4px', 'important'); | |||
isMenuOpen = true; | |||
} | |||
function closeMenu() { | |||
const checkbox = document.getElementById('vector-main-menu-dropdown-checkbox'); | |||
const dropdown = getDropdownElement(); | |||
if (!dropdown) return; | |||
if (checkbox) { | |||
checkbox.checked = false; | |||
checkbox.dispatchEvent(new Event('change', { bubbles: true })); | |||
} | |||
dropdown.style.removeProperty('position'); | |||
dropdown.style.removeProperty('top'); | |||
dropdown.style.removeProperty('left'); | |||
dropdown.style.removeProperty('z-index'); | |||
dropdown.style.removeProperty('max-height'); | |||
dropdown.style.removeProperty('overflow-y'); | |||
dropdown.style.removeProperty('background-color'); | |||
dropdown.style.removeProperty('box-shadow'); | |||
dropdown.style.removeProperty('border-radius'); | |||
dropdown.style.setProperty('display', 'none', 'important'); | |||
dropdown.style.setProperty('visibility', 'hidden', 'important'); | |||
dropdown.style.setProperty('opacity', '0', 'important'); | |||
isMenuOpen = false; | |||
} | |||
function initStickyBurger() { | |||
forceUnpinSidebar(); | |||
const stickyHeader = document.getElementById('vector-sticky-header'); | |||
if (!stickyHeader) return false; | |||
if (document.getElementById('vanipedia-sticky-main-menu')) return true; | |||
// 1. Create burger button | |||
const menuBtn = document.createElement('div'); | |||
menuBtn.id = 'vanipedia-sticky-main-menu'; | |||
menuBtn.title = 'Main menu'; | |||
menuBtn.style.cursor = 'pointer'; | |||
menuBtn.className = 'vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--icon-only'; | |||
// 2. Add hamburger icon | |||
const iconSpan = document.createElement('span'); | |||
iconSpan.className = 'vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu'; | |||
menuBtn.appendChild(iconSpan); | |||
// 3. Toggle click handler (1st click opens, 2nd click rolls back) | |||
menuBtn.addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
if (isMenuOpen) { | |||
closeMenu(); | |||
} else { | |||
openMenu(menuBtn); | |||
} | |||
}); | |||
// 4. Inject into start container | |||
let startContainer = stickyHeader.querySelector('.vector-sticky-header-start'); | |||
if (!startContainer) { | |||
startContainer = document.createElement('div'); | |||
startContainer.className = 'vector-sticky-header-start'; | |||
stickyHeader.insertBefore(startContainer, stickyHeader.firstChild); | |||
} | |||
startContainer.insertBefore(menuBtn, startContainer.firstChild); | |||
return true; | |||
} | |||
// 5. Close when clicking anywhere outside | |||
document.addEventListener('click', function (e) { | |||
if (!isMenuOpen) return; | |||
const menuBtn = document.getElementById('vanipedia-sticky-main-menu'); | |||
const dropdown = getDropdownElement(); | |||
if (dropdown && menuBtn && !menuBtn.contains(e.target) && !dropdown.contains(e.target)) { | |||
closeMenu(); | |||
} | |||
}); | |||
// 6. Bootstrap initializer | |||
if (!initStickyBurger()) { | |||
const checkTimer = setInterval(function () { | |||
if (initStickyBurger()) { | |||
clearInterval(checkTimer); | |||
} | |||
}, 100); | |||
setTimeout(function () { clearInterval(checkTimer); }, 4000); | |||
} | |||
mw.hook('wikipage.content').add(initStickyBurger); | |||
$(initStickyBurger); | |||
})(); | |||
/* Floating "Back to Top" Button Controller */ | |||
(function () { | |||
function initBackToTop() { | |||
if (document.getElementById('vanipedia-back-to-top')) return; | |||
// 1. Create button element with an up-arrow SVG icon | |||
const btn = document.createElement('button'); | |||
btn.id = 'vanipedia-back-to-top'; | |||
btn.title = 'Back to top'; | |||
btn.setAttribute('aria-label', 'Back to top'); | |||
btn.innerHTML = ` | |||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |||
<polyline points="18 15 12 9 6 15"></polyline> | |||
</svg> | |||
`; | |||
// 2. Smooth scroll to top on click | |||
btn.addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
window.scrollTo({ top: 0, behavior: 'smooth' }); | |||
}); | |||
// 3. Show button only after scrolling down 300px | |||
function toggleVisibility() { | |||
if (window.scrollY > 300) { | |||
btn.classList.add('visible'); | |||
} else { | |||
btn.classList.remove('visible'); | |||
} | |||
} | |||
window.addEventListener('scroll', toggleVisibility, { passive: true }); | |||
toggleVisibility(); | |||
document.body.appendChild(btn); | |||
} | |||
$(initBackToTop); | |||
mw.hook('wikipage.content').add(initBackToTop); | |||
})(); | |||
(function () { | |||
if (!document.getElementById('vani-home')) { | |||
return; | |||
} | |||
/* ===================================================== | |||
ROTATING HERO PHOTOGRAPHS | |||
===================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const vaniHeroImages = [ | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-01.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-02.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-03.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-04.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-05.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-06.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-07.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-08.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-09.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-10.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-11.jpg', | |||
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-12.jpg' | |||
]; | |||
const heroA = document.querySelector('.vh-photo-a'); | |||
const heroB = document.querySelector('.vh-photo-b'); | |||
if (heroA && heroB && vaniHeroImages.length) { | |||
let currentImage = -1; | |||
let activeLayer = heroA; | |||
function chooseHeroImage() { | |||
let next; | |||
do { | |||
next = Math.floor( | |||
Math.random() * vaniHeroImages.length | |||
); | |||
} while ( | |||
next === currentImage && | |||
vaniHeroImages.length > 1 | |||
); | |||
return next; | |||
} | |||
function changeHero(firstLoad) { | |||
const next = chooseHeroImage(); | |||
const image = vaniHeroImages[next]; | |||
if (firstLoad) { | |||
heroA.style.backgroundImage = | |||
'url("' + image + '")'; | |||
heroA.style.opacity = '1'; | |||
heroB.style.opacity = '0'; | |||
activeLayer = heroA; | |||
currentImage = next; | |||
return; | |||
} | |||
const incoming = | |||
activeLayer === heroA ? heroB : heroA; | |||
incoming.style.backgroundImage = | |||
'url("' + image + '")'; | |||
incoming.style.opacity = '1'; | |||
activeLayer.style.opacity = '0'; | |||
activeLayer = incoming; | |||
currentImage = next; | |||
} | |||
changeHero(true); | |||
window.setInterval(function () { | |||
changeHero(false); | |||
}, 10000); | |||
} | |||
} | |||
/* ===================================================== | |||
ROTATING ŚRĪLA PRABHUPĀDA SUBTITLE | |||
===================================================== */ | |||
const vaniSubtitles = [ | |||
'His words still speak to us today.', | |||
'His vāṇī brings closer to Kṛṣṇa.<br>His vision gives direction in lives.', | |||
'Words spoken decades ago.<br>Wisdom that speaks personally today.', | |||
'His vāṇī lives when we hear it and share with others.', | |||
'There is always more to discover.<br>More to hear. More to understand. More to live.', | |||
'His teachings give direction.<br>His example gives courage to follow.', | |||
'From his heart —<br>a treasure of teachings for every generation.', | |||
'His vāṇī. His compassion. His example.', | |||
'His vāṇī lives on. His teachings guide us.<br>His mission continues.', | |||
'His words cross decades and continents —<br>still reaching hearts, still changing lives.', | |||
'His words continue to speak.<br>His teachings continue to guide.', | |||
'He gave us a great treasure.<br>Vanipedia invites you to enter it.', | |||
'A lifetime of teachings.<br>Invitation to hear him with an open heart.', | |||
'His words illuminate the path.<br>His example inspires us to walk it.', | |||
'He carried Kṛṣṇa consciousness across the world.<br>His vāṇī continues that journey today.', | |||
'He lives on through his words,<br>his teachings and the mission he gave us.', | |||
'His words continue to speak.<br>His teachings continue to guide.', | |||
'Preserving and sharing Śrīla Prabhupāda’s vāṇī<br>so that generations may continue to hear from him.' | |||
]; | |||
const vaniSubtitle = | |||
document.getElementById('vh-subtitle-rotation'); | |||
if (vaniSubtitle) { | |||
let currentSubtitle = -1; | |||
function chooseSubtitle() { | |||
let next; | |||
do { | |||
next = Math.floor( | |||
Math.random() * vaniSubtitles.length | |||
); | |||
} while ( | |||
next === currentSubtitle && | |||
vaniSubtitles.length > 1 | |||
); | |||
return next; | |||
} | |||
function showSubtitle(firstLoad) { | |||
const next = chooseSubtitle(); | |||
if (firstLoad) { | |||
currentSubtitle = next; | |||
vaniSubtitle.innerHTML = vaniSubtitles[next]; | |||
vaniSubtitle.classList.remove('is-changing'); | |||
return; | |||
} | |||
// Fade out | |||
vaniSubtitle.classList.add('is-changing'); | |||
window.setTimeout(function () { | |||
vaniSubtitle.innerHTML = vaniSubtitles[next]; | |||
currentSubtitle = next; | |||
requestAnimationFrame(function () { | |||
requestAnimationFrame(function () { | |||
vaniSubtitle.classList.remove('is-changing'); | |||
}); | |||
}); | |||
}, 450); | |||
} | |||
/* Initial sentence */ | |||
showSubtitle(true); | |||
/* Change every 10 seconds */ | |||
window.setInterval(function () { | |||
showSubtitle(false); | |||
}, 12000); | |||
} | |||
/* ========================================================== | |||
MAIN PAGE — REMOVE NATIVE LINK TOOLTIPS | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
function removeMainPageLinkTitles() { | |||
document | |||
.querySelectorAll('#vani-home a[title]') | |||
.forEach(function (link) { | |||
link.removeAttribute('title'); | |||
}); | |||
} | |||
$(removeMainPageLinkTitles); | |||
mw.hook('wikipage.content').add(function () { | |||
removeMainPageLinkTitles(); | |||
}); | |||
} | |||
/* ===================================================== | |||
RANDOM THEMATIC ARTICLE SPOTLIGHT | |||
===================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const discoverBox = document.getElementById('vh-random-article'); | |||
if (discoverBox) { | |||
let articleLinks = []; | |||
let currentHref = null; | |||
const sourceUrl = | |||
mw.util.getUrl('Category:Vanipedia Gemini - Articles'); | |||
function loadArticleLinks() { | |||
return fetch(sourceUrl) | |||
.then(function (response) { | |||
return response.text(); | |||
}) | |||
.then(function (html) { | |||
const parser = new DOMParser(); | |||
const doc = parser.parseFromString(html, 'text/html'); | |||
const links = Array.from( | |||
doc.querySelectorAll('#mw-content-text a') | |||
); | |||
articleLinks = links | |||
.map(function (a) { | |||
return { | |||
title: a.textContent.trim(), | |||
href: a.getAttribute('href') | |||
}; | |||
}) | |||
.filter(function (item) { | |||
if (!item.title || !item.href) { | |||
return false; | |||
} | |||
if (!item.href.startsWith('/wiki/')) { | |||
return false; | |||
} | |||
if (item.href.includes('Category:')) { | |||
return false; | |||
} | |||
if (item.href.includes('Special:')) { | |||
return false; | |||
} | |||
return true; | |||
}); | |||
console.log( | |||
'Spotlight links loaded:', | |||
articleLinks.length | |||
); | |||
}); | |||
} | |||
function chooseRandomArticle() { | |||
if (!articleLinks.length) { | |||
return null; | |||
} | |||
let chosen; | |||
do { | |||
chosen = | |||
articleLinks[ | |||
Math.floor(Math.random() * articleLinks.length) | |||
]; | |||
} while ( | |||
chosen.href === currentHref && | |||
articleLinks.length > 1 | |||
); | |||
return chosen; | |||
} | |||
function getShortTitle(youtubeUrl) { | |||
const url = | |||
'https://www.youtube.com/oembed' + | |||
'?url=' + | |||
encodeURIComponent(youtubeUrl) + | |||
'&format=json'; | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'YouTube title HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
return ( | |||
data && | |||
data.title | |||
? data.title.trim() | |||
: '' | |||
); | |||
}) | |||
.catch(function(error) { | |||
console.warn( | |||
'Could not load Short title:', | |||
error | |||
); | |||
return ''; | |||
}); | |||
} | |||
function showRandomArticle() { | |||
const article = chooseRandomArticle(); | |||
if (!article) { | |||
return; | |||
} | |||
currentHref = article.href; | |||
const title = | |||
decodeURIComponent( | |||
article.href | |||
.replace(/^\/wiki\//, '') | |||
.replace(/_/g, ' ') | |||
); | |||
const apiUrl = | |||
mw.util.wikiScript('api') + | |||
'?action=query' + | |||
'&format=json' + | |||
'&prop=extracts' + | |||
'&exintro=1' + | |||
'&explaintext=1' + | |||
'&exchars=260' + | |||
'&titles=' + | |||
encodeURIComponent(title); | |||
fetch(apiUrl) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if (!data.query || !data.query.pages) { | |||
return; | |||
} | |||
const page = Object.values(data.query.pages)[0]; | |||
/* Do not display pages without a real extract */ | |||
if ( | |||
!page || | |||
!page.extract || | |||
page.extract.trim().length < 40 | |||
) { | |||
showRandomArticle(); | |||
return; | |||
} | |||
const extract = page.extract.trim(); | |||
/* Fetch is finished. NOW begin the fade. */ | |||
discoverBox.classList.add('is-changing'); | |||
window.setTimeout(function () { | |||
discoverBox.innerHTML = | |||
'<a class="vh-random-article-link" href="' + | |||
article.href + | |||
'">' + | |||
'<div class="vh-random-article-title">' + | |||
escapeHtml(article.title) + | |||
'</div>' + | |||
'<div class="vh-random-article-extract">' + | |||
escapeHtml(extract) + | |||
'</div>' + | |||
'<div class="vh-random-article-more">' + | |||
'Read article →' + | |||
'</div>' + | |||
'</a>'; | |||
requestAnimationFrame(function () { | |||
discoverBox.classList.remove('is-changing'); | |||
}); | |||
}, 500); | |||
}) | |||
.catch(function () { | |||
/* Simply try another article */ | |||
showRandomArticle(); | |||
}); | |||
} | |||
function escapeHtml(text) { | |||
const div = document.createElement('div'); | |||
div.textContent = text; | |||
return div.innerHTML; | |||
} | |||
loadArticleLinks() | |||
.then(function () { | |||
if (!articleLinks.length) { | |||
return; | |||
} | |||
showRandomArticle(); | |||
window.setInterval(function () { | |||
showRandomArticle(); | |||
}, 11000); | |||
}) | |||
.catch(function (error) { | |||
console.error( | |||
'Spotlight failed:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
/* ===================================================== | |||
FROM HIS LETTERS | |||
Random Śrīla Prabhupāda letter from Vanisource | |||
===================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const letterBox = | |||
document.getElementById('vh-random-letter'); | |||
if (letterBox) { | |||
const vanisourceApi = | |||
'https://vanisource.org/w/api.php'; | |||
const rootCategory = | |||
'Category:Letters from Srila Prabhupada - by person'; | |||
let recipientCategories = []; | |||
let currentLetter = ''; | |||
let letterChanging = false; | |||
/* ------------------------------------------------- | |||
LOAD RECIPIENT CATEGORIES | |||
------------------------------------------------- */ | |||
function loadLetterRecipients(cmcontinue) { | |||
let url = | |||
vanisourceApi + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent(rootCategory) + | |||
'&cmtype=subcat' + | |||
'&cmlimit=500'; | |||
if (cmcontinue) { | |||
url += | |||
'&cmcontinue=' + | |||
encodeURIComponent(cmcontinue); | |||
} | |||
return fetch(url) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if ( | |||
data.query && | |||
data.query.categorymembers | |||
) { | |||
data.query.categorymembers | |||
.forEach(function (item) { | |||
if ( | |||
item.title && | |||
item.title.endsWith( | |||
' - Letters' | |||
) | |||
) { | |||
recipientCategories.push( | |||
item.title | |||
); | |||
} | |||
}); | |||
} | |||
/* Continue if more than 500 */ | |||
if ( | |||
data.continue && | |||
data.continue.cmcontinue | |||
) { | |||
return loadLetterRecipients( | |||
data.continue.cmcontinue | |||
); | |||
} | |||
}); | |||
} | |||
/* ------------------------------------------------- | |||
CHOOSE RANDOM RECIPIENT | |||
------------------------------------------------- */ | |||
function chooseRecipient() { | |||
if (!recipientCategories.length) { | |||
return null; | |||
} | |||
return recipientCategories[ | |||
Math.floor( | |||
Math.random() * | |||
recipientCategories.length | |||
) | |||
]; | |||
} | |||
/* ------------------------------------------------- | |||
GET LETTERS FOR THAT RECIPIENT | |||
------------------------------------------------- */ | |||
function getRandomLetter(category) { | |||
const url = | |||
vanisourceApi + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent(category) + | |||
'&cmnamespace=0' + | |||
'&cmtype=page' + | |||
'&cmlimit=500'; | |||
return fetch(url) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if ( | |||
!data.query || | |||
!data.query.categorymembers || | |||
!data.query.categorymembers.length | |||
) { | |||
return null; | |||
} | |||
const letters = | |||
data.query.categorymembers; | |||
let letter; | |||
do { | |||
letter = | |||
letters[ | |||
Math.floor( | |||
Math.random() * | |||
letters.length | |||
) | |||
]; | |||
} while ( | |||
currentLetter && | |||
letter.title === currentLetter && | |||
letters.length > 1 | |||
); | |||
return letter; | |||
}); | |||
} | |||
/* ------------------------------------------------- | |||
RECIPIENT NAME | |||
------------------------------------------------- */ | |||
function getRecipientName(category) { | |||
return category | |||
.replace(/^Category:/, '') | |||
.replace(/ - Letters$/, '') | |||
.trim(); | |||
} | |||
/* ------------------------------------------------- | |||
TRY TO EXTRACT DATE FROM LETTER PAGE TITLE | |||
Supports dates at beginning such as: | |||
690101 | |||
740516 | |||
etc. | |||
------------------------------------------------- */ | |||
function getLetterDate(title) { | |||
const match = | |||
title.match( | |||
/(?:^|\/)(\d{2})(\d{2})(\d{2})(?:\D|$)/ | |||
); | |||
if (!match) { | |||
return ''; | |||
} | |||
const yy = | |||
parseInt(match[1], 10); | |||
const month = | |||
parseInt(match[2], 10); | |||
const day = | |||
parseInt(match[3], 10); | |||
if ( | |||
month < 1 || | |||
month > 12 || | |||
day < 1 || | |||
day > 31 | |||
) { | |||
return ''; | |||
} | |||
const year = | |||
1900 + yy; | |||
const months = [ | |||
'January', | |||
'February', | |||
'March', | |||
'April', | |||
'May', | |||
'June', | |||
'July', | |||
'August', | |||
'September', | |||
'October', | |||
'November', | |||
'December' | |||
]; | |||
return ( | |||
day + | |||
' ' + | |||
months[month - 1] + | |||
' ' + | |||
year | |||
); | |||
} | |||
/* ------------------------------------------------- | |||
GET ACTUAL LETTER CONTENT | |||
------------------------------------------------- */ | |||
function getLetterPassage(letterTitle) { | |||
const url = | |||
vanisourceApi + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(letterTitle) + | |||
'&prop=text'; | |||
return fetch(url) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.text || | |||
!data.parse.text['*'] | |||
) { | |||
return ''; | |||
} | |||
const wrapper = | |||
document.createElement('div'); | |||
wrapper.innerHTML = | |||
data.parse.text['*']; | |||
/* | |||
* Remove things which definitely are | |||
* not part of the actual letter. | |||
*/ | |||
wrapper.querySelectorAll( | |||
'table, .navbox, .toc, .mw-editsection, ' + | |||
'.metadata, .noprint, style, script' | |||
).forEach(function (element) { | |||
element.remove(); | |||
}); | |||
/* | |||
* Collect useful paragraphs. | |||
*/ | |||
let paragraphs = | |||
Array.from( | |||
wrapper.querySelectorAll('p') | |||
) | |||
.map(function (p) { | |||
return p.textContent | |||
.replace(/\s+/g, ' ') | |||
.trim(); | |||
}) | |||
.filter(function (text) { | |||
if (text.length < 120) { | |||
return false; | |||
} | |||
const lower = text.toLowerCase(); | |||
/* Standard opening/closing formalities */ | |||
if ( | |||
lower.startsWith('dear ') || | |||
lower.startsWith('my dear ') || | |||
lower.includes('please accept my blessings') || | |||
lower.includes('please accept my humble') || | |||
lower.includes('i beg to acknowledge receipt') || | |||
lower.includes('i am in due receipt') || | |||
lower.includes('your ever well-wisher') || | |||
lower.includes('hoping this will meet you') || | |||
lower.includes('yours in the service') || | |||
lower.includes('your servant') | |||
) { | |||
return false; | |||
} | |||
/* Addresses / letter headers */ | |||
if ( | |||
lower.includes('iskcon') || | |||
lower.includes('international society for krishna consciousness') || | |||
lower.includes('founder-acarya') || | |||
lower.includes('founder acharya') || | |||
lower.includes('a.c. bhaktivedanta swami') || | |||
lower.includes('tridandi goswami') || | |||
lower.includes('tridandi gosvami') || | |||
lower.includes('bhaktivedanta manor') || | |||
lower.includes('hare krishna land') | |||
) { | |||
return false; | |||
} | |||
/* Looks too much like an address */ | |||
const addressWords = [ | |||
'road', | |||
'street', | |||
'avenue', | |||
'building', | |||
'apartment', | |||
'bombay', | |||
'calcutta', | |||
'los angeles', | |||
'new york', | |||
'london' | |||
]; | |||
let addressHits = 0; | |||
addressWords.forEach(function (word) { | |||
if (lower.includes(word)) { | |||
addressHits++; | |||
} | |||
}); | |||
if (addressHits >= 2) { | |||
return false; | |||
} | |||
/* | |||
* Require something resembling actual prose: | |||
* at least two sentence endings. | |||
*/ | |||
const sentences = | |||
(text.match(/[.!?](?:\s|$)/g) || []).length; | |||
if (sentences < 2) { | |||
return false; | |||
} | |||
return true; | |||
}); | |||
if (!paragraphs.length) { | |||
return ''; | |||
} | |||
/* | |||
* Prefer reasonably substantial passages. | |||
*/ | |||
let preferred = | |||
paragraphs.filter(function (text) { | |||
return ( | |||
text.length >= 180 && | |||
text.length <= 400 | |||
); | |||
}); | |||
if (!preferred.length) { | |||
preferred = paragraphs; | |||
} | |||
/* | |||
* Random meaningful paragraph from | |||
* within the letter. | |||
*/ | |||
let passage = | |||
preferred[ | |||
Math.floor( | |||
Math.random() * | |||
preferred.length | |||
) | |||
]; | |||
/* | |||
* Prevent enormous passage in narrow | |||
* left column. | |||
*/ | |||
if (passage.length > 300) { | |||
passage = | |||
passage.substring(0, 300); | |||
const lastSpace = | |||
passage.lastIndexOf(' '); | |||
if (lastSpace > 250) { | |||
passage = | |||
passage.substring( | |||
0, | |||
lastSpace | |||
); | |||
} | |||
passage += '…'; | |||
} | |||
return passage; | |||
}); | |||
} | |||
/* ------------------------------------------------- | |||
SAFE HTML | |||
------------------------------------------------- */ | |||
function escapeLetterHtml(text) { | |||
const div = | |||
document.createElement('div'); | |||
div.textContent = text; | |||
return div.innerHTML; | |||
} | |||
/* ------------------------------------------------- | |||
BUILD VANISOURCE URL | |||
------------------------------------------------- */ | |||
function getLetterUrl(title) { | |||
return ( | |||
'https://vanisource.org/wiki/' + | |||
encodeURIComponent( | |||
title.replace(/ /g, '_') | |||
) | |||
.replace(/%2F/g, '/') | |||
.replace(/%3A/g, ':') | |||
); | |||
} | |||
/* ------------------------------------------------- | |||
FIND A VALID RANDOM LETTER | |||
------------------------------------------------- */ | |||
function prepareRandomLetter(attempt) { | |||
attempt = attempt || 0; | |||
/* | |||
* Prevent an endless retry loop if Vanisource | |||
* unexpectedly returns unusable pages. | |||
*/ | |||
if (attempt > 15) { | |||
return Promise.resolve(null); | |||
} | |||
const category = | |||
chooseRecipient(); | |||
if (!category) { | |||
return Promise.resolve(null); | |||
} | |||
return getRandomLetter(category) | |||
.then(function (letter) { | |||
if (!letter || !letter.title) { | |||
return prepareRandomLetter( | |||
attempt + 1 | |||
); | |||
} | |||
return getLetterPassage( | |||
letter.title | |||
) | |||
.then(function (passage) { | |||
if ( | |||
!passage || | |||
passage.length < 80 | |||
) { | |||
return prepareRandomLetter( | |||
attempt + 1 | |||
); | |||
} | |||
return { | |||
title: | |||
letter.title, | |||
recipient: | |||
getRecipientName( | |||
category | |||
), | |||
date: | |||
getLetterDate( | |||
letter.title | |||
), | |||
passage: | |||
passage, | |||
href: | |||
getLetterUrl( | |||
letter.title | |||
) | |||
}; | |||
}); | |||
}); | |||
} | |||
/* ------------------------------------------------- | |||
DISPLAY LETTER | |||
------------------------------------------------- */ | |||
function showRandomLetter() { | |||
if (letterChanging) { | |||
return; | |||
} | |||
letterChanging = true; | |||
/* | |||
* Prepare the next letter while the current | |||
* one remains fully visible. | |||
*/ | |||
prepareRandomLetter(0) | |||
.then(function (letter) { | |||
if (!letter) { | |||
letterChanging = false; | |||
return; | |||
} | |||
currentLetter = | |||
letter.title; | |||
/* | |||
* Only start fading AFTER the new | |||
* content is ready. | |||
*/ | |||
letterBox.classList.add( | |||
'is-changing' | |||
); | |||
window.setTimeout( | |||
function () { | |||
let meta = ''; | |||
if (letter.date) { | |||
meta = | |||
'<div class="vh-random-letter-date">' + | |||
escapeLetterHtml( | |||
letter.date | |||
) + | |||
'</div>'; | |||
} | |||
letterBox.innerHTML = | |||
'<a class="vh-random-letter-link" ' + | |||
'href="' + | |||
letter.href + | |||
'">' + | |||
'<div class="vh-random-letter-person">' + | |||
'To ' + | |||
escapeLetterHtml( | |||
letter.recipient | |||
) + | |||
'</div>' + | |||
meta + | |||
'<div class="vh-random-letter-text">' + | |||
escapeLetterHtml( | |||
letter.passage | |||
) + | |||
'</div>' + | |||
'<div class="vh-random-letter-more">' + | |||
'Read letter →' + | |||
'</div>' + | |||
'</a>'; | |||
requestAnimationFrame( | |||
function () { | |||
letterBox.classList.remove( | |||
'is-changing' | |||
); | |||
letterChanging = false; | |||
} | |||
); | |||
}, | |||
450 | |||
); | |||
}) | |||
.catch(function (error) { | |||
console.error( | |||
'Letter feature failed:', | |||
error | |||
); | |||
letterChanging = false; | |||
}); | |||
} | |||
/* ------------------------------------------------- | |||
START | |||
------------------------------------------------- */ | |||
loadLetterRecipients() | |||
.then(function () { | |||
if (!recipientCategories.length) { | |||
console.warn( | |||
'No Vanisource letter recipients found.' | |||
); | |||
return; | |||
} | |||
console.log( | |||
'Letter recipient categories loaded:', | |||
recipientCategories.length | |||
); | |||
/* | |||
* First letter immediately. | |||
*/ | |||
showRandomLetter(); | |||
/* | |||
* New letter every 10 seconds. | |||
*/ | |||
window.setInterval( | |||
showRandomLetter, | |||
10000 | |||
); | |||
}) | |||
.catch(function (error) { | |||
console.error( | |||
'Could not load Vanisource letters:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
/* ========================================================== | |||
VIRTUAL CONTINUOUS ŚRĪLA PRABHUPĀDA TIMELINE | |||
Includes 1896–1964 + 1965–1977 | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const timelineWindow = | |||
document.querySelector('.vh-lifeline-window'); | |||
const timelineTrack = | |||
document.getElementById('vh-timeline-track'); | |||
const yearButtons = | |||
document.querySelectorAll( | |||
'#vh-timeline-years .vh-year' | |||
); | |||
if (timelineWindow && timelineTrack) { | |||
const API = | |||
'https://vanipedia.org/w/api.php'; | |||
const ITEM_WIDTH = 220; | |||
const AUTO_SPEED = 52; | |||
const VISIBLE_ITEMS = 22; | |||
let events = []; | |||
let nodes = []; | |||
let firstIndex = 0; | |||
let offsetX = 0; | |||
let dragging = false; | |||
let dragMoved = false; | |||
let pointerStartX = 0; | |||
let previousPointerX = 0; | |||
let lastFrame = | |||
performance.now(); | |||
let animationId = null; | |||
/* ================================================== | |||
TEXT HELPERS | |||
================================================== */ | |||
function cleanText(text) { | |||
return text | |||
.replace(/\s+/g, ' ') | |||
.replace(/^[\s\-–—:,]+/, '') | |||
.trim(); | |||
} | |||
function shortenText(text) { | |||
text = | |||
cleanText(text); | |||
if (text.length <= 95) { | |||
return text; | |||
} | |||
let result = | |||
text.substring(0, 95); | |||
const space = | |||
result.lastIndexOf(' '); | |||
if (space > 70) { | |||
result = | |||
result.substring( | |||
0, | |||
space | |||
); | |||
} | |||
return result + '…'; | |||
} | |||
/* ================================================== | |||
DEFAULT PAGE LINK | |||
================================================== */ | |||
function makePageUrl(page) { | |||
return ( | |||
'https://vanipedia.org/wiki/' + | |||
encodeURIComponent( | |||
page.replace(/ /g, '_') | |||
) | |||
.replace(/%2F/g, '/') | |||
.replace(/%3A/g, ':') | |||
); | |||
} | |||
/* ================================================== | |||
FIND BEST LINK INSIDE ENTRY | |||
================================================== */ | |||
function getUsefulLink( | |||
element, | |||
fallback | |||
) { | |||
const usefulLink = | |||
Array | |||
.from( | |||
element.querySelectorAll('a') | |||
) | |||
.find( | |||
function (a) { | |||
const h = | |||
a.getAttribute( | |||
'href' | |||
); | |||
return ( | |||
h && | |||
!h.includes( | |||
'action=edit' | |||
) && | |||
!h.includes( | |||
'redlink=1' | |||
) | |||
); | |||
} | |||
); | |||
if (!usefulLink) { | |||
return fallback; | |||
} | |||
const rawHref = | |||
usefulLink.getAttribute( | |||
'href' | |||
); | |||
if (!rawHref) { | |||
return fallback; | |||
} | |||
try { | |||
return new URL( | |||
rawHref, | |||
'https://vanipedia.org' | |||
).href; | |||
} catch (e) { | |||
return fallback; | |||
} | |||
} | |||
/* ================================================== | |||
FETCH 1896–1964 | |||
Main timeline page | |||
================================================== */ | |||
function fetchEarlyTimeline() { | |||
const page = | |||
"Timeline of Srila Prabhupada's Life"; | |||
const url = | |||
API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(page) + | |||
'&prop=text'; | |||
return fetch(url) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.text || | |||
!data.parse.text['*'] | |||
) { | |||
return []; | |||
} | |||
const temp = | |||
document.createElement('div'); | |||
temp.innerHTML = | |||
data.parse.text['*']; | |||
const result = []; | |||
temp | |||
.querySelectorAll('li') | |||
.forEach(function (li) { | |||
const fullText = | |||
cleanText( | |||
li.textContent | |||
); | |||
/* | |||
* Expected examples: | |||
* | |||
* 1896 - September 01 - Calcutta - ... | |||
* 1922 - Meets ... | |||
* 1944 - February 14 - Publishes ... | |||
* 1959 - September 17 - ... | |||
* | |||
* We only keep 1896–1964. | |||
*/ | |||
const match = | |||
fullText.match( | |||
/^((?:18|19)\d{2})\s*[-–—]\s*(.+)$/i | |||
); | |||
if (!match) { | |||
return; | |||
} | |||
const year = | |||
parseInt( | |||
match[1], | |||
10 | |||
); | |||
if ( | |||
year < 1896 || | |||
year > 1964 | |||
) { | |||
return; | |||
} | |||
let remainder = | |||
cleanText( | |||
match[2] | |||
); | |||
if (!remainder) { | |||
return; | |||
} | |||
/* | |||
* Try to extract a date | |||
* from the remainder. | |||
*/ | |||
let displayDate = | |||
String(year); | |||
let description = | |||
remainder; | |||
const monthMatch = | |||
remainder.match( | |||
/^(January|February|March|April|May|June|July|August|September|October|November|December)(?:\s+(\d{1,2}))?\s*[-–—]\s*(.+)$/i | |||
); | |||
if (monthMatch) { | |||
const month = | |||
monthMatch[1]; | |||
const day = | |||
monthMatch[2] | |||
? monthMatch[2] | |||
: ''; | |||
description = | |||
cleanText( | |||
monthMatch[3] | |||
); | |||
displayDate = | |||
month; | |||
if (day) { | |||
displayDate += | |||
' ' + | |||
day; | |||
} | |||
displayDate += | |||
', ' + | |||
year; | |||
} else { | |||
/* | |||
* If no month is present, | |||
* show year only. | |||
*/ | |||
description = | |||
remainder; | |||
} | |||
description = | |||
shortenText( | |||
description | |||
); | |||
if (!description) { | |||
return; | |||
} | |||
const fallback = | |||
makePageUrl(page); | |||
const href = | |||
getUsefulLink( | |||
li, | |||
fallback | |||
); | |||
result.push({ | |||
year: | |||
year, | |||
group: | |||
'1896-1964', | |||
date: | |||
displayDate, | |||
text: | |||
description, | |||
href: | |||
href | |||
}); | |||
}); | |||
return result; | |||
}); | |||
} | |||
/* ================================================== | |||
FETCH ONE YEAR: 1965–1977 | |||
================================================== */ | |||
function fetchYear(year) { | |||
const page = | |||
"Timeline of Srila Prabhupada's Life - " + | |||
year; | |||
const url = | |||
API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(page) + | |||
'&prop=text'; | |||
return fetch(url) | |||
.then(function (response) { | |||
return response.json(); | |||
}) | |||
.then(function (data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.text || | |||
!data.parse.text['*'] | |||
) { | |||
return []; | |||
} | |||
const temp = | |||
document.createElement('div'); | |||
temp.innerHTML = | |||
data.parse.text['*']; | |||
const result = []; | |||
temp | |||
.querySelectorAll('li') | |||
.forEach(function (li) { | |||
const fullText = | |||
cleanText( | |||
li.textContent | |||
); | |||
const match = | |||
fullText.match( | |||
/^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})(?:,?\s+([0-9]{1,2}(?::[0-9]{2})?(?:\s*[AP]\.?M\.?)?))?\s*[-–—]\s*(.+)$/i | |||
); | |||
if (!match) { | |||
return; | |||
} | |||
const month = | |||
match[1]; | |||
const day = | |||
match[2]; | |||
const time = | |||
match[3] | |||
? match[3].trim() | |||
: ''; | |||
const description = | |||
shortenText( | |||
match[4] | |||
); | |||
if (!description) { | |||
return; | |||
} | |||
const fallback = | |||
makePageUrl(page); | |||
const href = | |||
getUsefulLink( | |||
li, | |||
fallback | |||
); | |||
let date = | |||
month + | |||
' ' + | |||
day + | |||
', ' + | |||
year; | |||
if (time) { | |||
date += | |||
' · ' + | |||
time; | |||
} | |||
result.push({ | |||
year: | |||
year, | |||
group: | |||
String(year), | |||
date: | |||
date, | |||
text: | |||
description, | |||
href: | |||
href | |||
}); | |||
}); | |||
return result; | |||
}); | |||
} | |||
/* ================================================== | |||
CREATE NODE | |||
================================================== */ | |||
function createNode() { | |||
const link = | |||
document.createElement('a'); | |||
link.className = | |||
'vh-timeline-item'; | |||
link.draggable = | |||
false; | |||
const date = | |||
document.createElement('div'); | |||
date.className = | |||
'vh-timeline-date'; | |||
const dot = | |||
document.createElement('span'); | |||
dot.className = | |||
'vh-timeline-dot'; | |||
const text = | |||
document.createElement('div'); | |||
text.className = | |||
'vh-timeline-text'; | |||
link.appendChild(date); | |||
link.appendChild(dot); | |||
link.appendChild(text); | |||
link.addEventListener( | |||
'click', | |||
function (event) { | |||
if (dragMoved) { | |||
event.preventDefault(); | |||
} | |||
} | |||
); | |||
return link; | |||
} | |||
/* ================================================== | |||
POPULATE NODE | |||
================================================== */ | |||
function populateNode( | |||
node, | |||
eventIndex | |||
) { | |||
const total = | |||
events.length; | |||
eventIndex = | |||
( | |||
( | |||
eventIndex % | |||
total | |||
) + | |||
total | |||
) % | |||
total; | |||
const item = | |||
events[eventIndex]; | |||
node.href = | |||
item.href; | |||
node.dataset.year = | |||
item.year; | |||
node.dataset.group = | |||
item.group; | |||
node | |||
.querySelector( | |||
'.vh-timeline-date' | |||
) | |||
.textContent = | |||
item.date; | |||
node | |||
.querySelector( | |||
'.vh-timeline-text' | |||
) | |||
.textContent = | |||
item.text; | |||
} | |||
/* ================================================== | |||
BUILD VIRTUAL STRIP | |||
================================================== */ | |||
function buildVirtualTimeline() { | |||
timelineTrack.innerHTML = ''; | |||
nodes = []; | |||
const needed = | |||
Math.max( | |||
VISIBLE_ITEMS, | |||
Math.ceil( | |||
timelineWindow.clientWidth / | |||
ITEM_WIDTH | |||
) + 6 | |||
); | |||
for ( | |||
let i = 0; | |||
i < needed; | |||
i++ | |||
) { | |||
const node = | |||
createNode(); | |||
populateNode( | |||
node, | |||
firstIndex + i | |||
); | |||
timelineTrack.appendChild( | |||
node | |||
); | |||
nodes.push( | |||
node | |||
); | |||
} | |||
timelineTrack.style.width = | |||
( | |||
needed * | |||
ITEM_WIDTH | |||
) + | |||
'px'; | |||
offsetX = | |||
-ITEM_WIDTH; | |||
firstIndex = | |||
( | |||
firstIndex - | |||
1 + | |||
events.length | |||
) % | |||
events.length; | |||
refreshAllNodes(); | |||
draw(); | |||
} | |||
function refreshAllNodes() { | |||
nodes.forEach( | |||
function ( | |||
node, | |||
position | |||
) { | |||
populateNode( | |||
node, | |||
firstIndex + | |||
position | |||
); | |||
} | |||
); | |||
} | |||
/* ================================================== | |||
RECYCLE LEFT | |||
================================================== */ | |||
function recycleLeft() { | |||
const node = | |||
nodes.shift(); | |||
nodes.push( | |||
node | |||
); | |||
timelineTrack.appendChild( | |||
node | |||
); | |||
firstIndex = | |||
( | |||
firstIndex + | |||
1 | |||
) % | |||
events.length; | |||
populateNode( | |||
node, | |||
firstIndex + | |||
nodes.length - | |||
1 | |||
); | |||
offsetX += | |||
ITEM_WIDTH; | |||
} | |||
/* ================================================== | |||
RECYCLE RIGHT | |||
================================================== */ | |||
function recycleRight() { | |||
const node = | |||
nodes.pop(); | |||
firstIndex = | |||
( | |||
firstIndex - | |||
1 + | |||
events.length | |||
) % | |||
events.length; | |||
nodes.unshift( | |||
node | |||
); | |||
timelineTrack.insertBefore( | |||
node, | |||
timelineTrack.firstChild | |||
); | |||
populateNode( | |||
node, | |||
firstIndex | |||
); | |||
offsetX -= | |||
ITEM_WIDTH; | |||
} | |||
function normalizePosition() { | |||
while ( | |||
offsetX <= | |||
-ITEM_WIDTH * 2 | |||
) { | |||
recycleLeft(); | |||
} | |||
while ( | |||
offsetX > 0 | |||
) { | |||
recycleRight(); | |||
} | |||
} | |||
/* ================================================== | |||
DRAW | |||
================================================== */ | |||
function draw() { | |||
timelineTrack.style.transform = | |||
'translate3d(' + | |||
offsetX + | |||
'px,0,0)'; | |||
} | |||
/* ================================================== | |||
ACTIVE YEAR BUTTON | |||
================================================== */ | |||
function setActiveButton(group) { | |||
yearButtons.forEach( | |||
function (button) { | |||
const value = | |||
button.classList.contains( | |||
'vh-year-early' | |||
) | |||
? '1896-1964' | |||
: button.textContent.trim(); | |||
button.classList.toggle( | |||
'is-active', | |||
value === group | |||
); | |||
} | |||
); | |||
} | |||
/* ================================================== | |||
JUMP TO YEAR / PERIOD | |||
================================================== */ | |||
function jumpToGroup(group) { | |||
if (!events.length) { | |||
return; | |||
} | |||
let targetIndex; | |||
if (group === '1896-1964') { | |||
targetIndex = | |||
events.findIndex( | |||
function (item) { | |||
return ( | |||
item.group === | |||
'1896-1964' | |||
); | |||
} | |||
); | |||
} else { | |||
const year = | |||
parseInt( | |||
group, | |||
10 | |||
); | |||
targetIndex = | |||
events.findIndex( | |||
function (item) { | |||
return ( | |||
item.year === | |||
year | |||
); | |||
} | |||
); | |||
} | |||
if (targetIndex === -1) { | |||
return; | |||
} | |||
/* | |||
* Put target near left side, | |||
* with one virtual buffer item before it. | |||
*/ | |||
firstIndex = | |||
( | |||
targetIndex - | |||
1 + | |||
events.length | |||
) % | |||
events.length; | |||
offsetX = | |||
-ITEM_WIDTH; | |||
refreshAllNodes(); | |||
draw(); | |||
setActiveButton( | |||
group | |||
); | |||
} | |||
/* ================================================== | |||
YEAR NAVIGATION | |||
================================================== */ | |||
yearButtons.forEach( | |||
function (button) { | |||
button.addEventListener( | |||
'click', | |||
function () { | |||
const value = | |||
button.classList.contains( | |||
'vh-year-early' | |||
) | |||
? '1896-1964' | |||
: button.textContent.trim(); | |||
jumpToGroup(value); | |||
} | |||
); | |||
} | |||
); | |||
/* ================================================== | |||
ANIMATION | |||
================================================== */ | |||
function animate(now) { | |||
let elapsed = | |||
( | |||
now - | |||
lastFrame | |||
) / | |||
1000; | |||
lastFrame = | |||
now; | |||
if (elapsed > 0.05) { | |||
elapsed = | |||
0.05; | |||
} | |||
if (!dragging) { | |||
offsetX -= | |||
AUTO_SPEED * | |||
elapsed; | |||
normalizePosition(); | |||
draw(); | |||
} | |||
animationId = | |||
requestAnimationFrame( | |||
animate | |||
); | |||
} | |||
/* ================================================== | |||
DRAGGING | |||
================================================== */ | |||
timelineWindow.addEventListener( | |||
'pointerdown', | |||
function (event) { | |||
if ( | |||
event.pointerType === | |||
'mouse' && | |||
event.button !== 0 | |||
) { | |||
return; | |||
} | |||
dragging = | |||
true; | |||
dragMoved = | |||
false; | |||
pointerStartX = | |||
event.clientX; | |||
previousPointerX = | |||
event.clientX; | |||
timelineWindow.classList.add( | |||
'is-dragging' | |||
); | |||
timelineWindow.setPointerCapture( | |||
event.pointerId | |||
); | |||
event.preventDefault(); | |||
} | |||
); | |||
timelineWindow.addEventListener( | |||
'pointermove', | |||
function (event) { | |||
if (!dragging) { | |||
return; | |||
} | |||
const currentX = | |||
event.clientX; | |||
const difference = | |||
currentX - | |||
previousPointerX; | |||
previousPointerX = | |||
currentX; | |||
if ( | |||
Math.abs( | |||
currentX - | |||
pointerStartX | |||
) > 4 | |||
) { | |||
dragMoved = | |||
true; | |||
} | |||
offsetX += | |||
difference; | |||
normalizePosition(); | |||
draw(); | |||
event.preventDefault(); | |||
} | |||
); | |||
function endDrag(event) { | |||
if (!dragging) { | |||
return; | |||
} | |||
dragging = | |||
false; | |||
timelineWindow.classList.remove( | |||
'is-dragging' | |||
); | |||
try { | |||
if ( | |||
timelineWindow.hasPointerCapture( | |||
event.pointerId | |||
) | |||
) { | |||
timelineWindow.releasePointerCapture( | |||
event.pointerId | |||
); | |||
} | |||
} catch (e) {} | |||
if (dragMoved) { | |||
window.setTimeout( | |||
function () { | |||
dragMoved = | |||
false; | |||
}, | |||
180 | |||
); | |||
} | |||
} | |||
timelineWindow.addEventListener( | |||
'pointerup', | |||
endDrag | |||
); | |||
timelineWindow.addEventListener( | |||
'pointercancel', | |||
endDrag | |||
); | |||
timelineWindow.addEventListener( | |||
'dragstart', | |||
function (event) { | |||
event.preventDefault(); | |||
} | |||
); | |||
/* ================================================== | |||
RESIZE | |||
================================================== */ | |||
let resizeTimer; | |||
window.addEventListener( | |||
'resize', | |||
function () { | |||
clearTimeout( | |||
resizeTimer | |||
); | |||
resizeTimer = | |||
setTimeout( | |||
function () { | |||
if ( | |||
events.length | |||
) { | |||
buildVirtualTimeline(); | |||
} | |||
}, | |||
150 | |||
); | |||
} | |||
); | |||
/* ================================================== | |||
LOAD ALL TIMELINE DATA | |||
================================================== */ | |||
const requests = []; | |||
/* | |||
* First: 1896–1964 | |||
*/ | |||
requests.push( | |||
fetchEarlyTimeline() | |||
); | |||
/* | |||
* Then: 1965–1977 | |||
*/ | |||
for ( | |||
let year = 1965; | |||
year <= 1977; | |||
year++ | |||
) { | |||
requests.push( | |||
fetchYear(year) | |||
); | |||
} | |||
Promise | |||
.all(requests) | |||
.then( | |||
function (results) { | |||
events = | |||
results.flat(); | |||
/* | |||
* Ensure chronological order | |||
* between groups. | |||
*/ | |||
events.sort( | |||
function (a, b) { | |||
return ( | |||
a.year - | |||
b.year | |||
); | |||
} | |||
); | |||
console.log( | |||
'TIMELINE EVENTS:', | |||
events.length | |||
); | |||
console.log( | |||
'EARLY EVENTS:', | |||
events.filter( | |||
function (item) { | |||
return ( | |||
item.group === | |||
'1896-1964' | |||
); | |||
} | |||
).length | |||
); | |||
if (!events.length) { | |||
console.warn( | |||
'No timeline events found.' | |||
); | |||
return; | |||
} | |||
/* | |||
* Start with 1965. | |||
*/ | |||
const startIndex = | |||
events.findIndex( | |||
function (item) { | |||
return ( | |||
item.year === | |||
1965 | |||
); | |||
} | |||
); | |||
firstIndex = | |||
startIndex >= 0 | |||
? startIndex | |||
: 0; | |||
buildVirtualTimeline(); | |||
setActiveButton( | |||
'1965' | |||
); | |||
lastFrame = | |||
performance.now(); | |||
animationId = | |||
requestAnimationFrame( | |||
animate | |||
); | |||
} | |||
) | |||
.catch( | |||
function (error) { | |||
console.error( | |||
'Timeline loading failed:', | |||
error | |||
); | |||
} | |||
); | |||
} | |||
} | |||
/* ========================================================= | |||
RANDOM NECTAR DROP — HOMEPAGE QUOTE | |||
========================================================= */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const quoteWrap = | |||
document.getElementById('vh-random-quote-wrap'); | |||
const quoteText = | |||
document.getElementById('vh-random-quote'); | |||
const quoteSource = | |||
document.getElementById('vh-random-quote-source'); | |||
if (quoteWrap && quoteText && quoteSource) { | |||
const API = | |||
'https://vanipedia.org/w/api.php'; | |||
const CATEGORY = | |||
'Category:Nectar Drops from Srila Prabhupada'; | |||
let nectarPages = []; | |||
let quoteChanging = false; | |||
let currentPage = ''; | |||
/* ----------------------------------------- | |||
LOAD ALL NECTAR DROP PAGES | |||
----------------------------------------- */ | |||
function loadNectarPages(cmcontinue) { | |||
let url = | |||
API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent(CATEGORY) + | |||
'&cmnamespace=0' + | |||
'&cmlimit=max'; | |||
if (cmcontinue) { | |||
url += | |||
'&cmcontinue=' + | |||
encodeURIComponent(cmcontinue); | |||
} | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'HTTP ' + response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
data.query && | |||
data.query.categorymembers | |||
) { | |||
nectarPages = | |||
nectarPages.concat( | |||
data.query.categorymembers | |||
); | |||
} | |||
if ( | |||
data.continue && | |||
data.continue.cmcontinue | |||
) { | |||
return loadNectarPages( | |||
data.continue.cmcontinue | |||
); | |||
} | |||
console.log( | |||
'Nectar Drop pages loaded:', | |||
nectarPages.length | |||
); | |||
}); | |||
} | |||
/* ----------------------------------------- | |||
GET RANDOM NECTAR DROP | |||
----------------------------------------- */ | |||
function getRandomNectarDrop() { | |||
if ( | |||
quoteChanging || | |||
!nectarPages.length | |||
) { | |||
return; | |||
} | |||
quoteChanging = true; | |||
let page; | |||
for ( | |||
let attempt = 0; | |||
attempt < 20; | |||
attempt++ | |||
) { | |||
const candidate = | |||
nectarPages[ | |||
Math.floor( | |||
Math.random() * | |||
nectarPages.length | |||
) | |||
]; | |||
if ( | |||
candidate && | |||
candidate.title !== currentPage | |||
) { | |||
page = candidate; | |||
break; | |||
} | |||
} | |||
if (!page) { | |||
quoteChanging = false; | |||
return; | |||
} | |||
/* ------------------------------------- | |||
GET RAW WIKITEXT | |||
------------------------------------- */ | |||
const url = | |||
API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(page.title) + | |||
'&prop=wikitext'; | |||
fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.wikitext || | |||
!data.parse.wikitext['*'] | |||
) { | |||
throw new Error( | |||
'No wikitext returned.' | |||
); | |||
} | |||
const wikitext = | |||
data.parse.wikitext['*']; | |||
/* --------------------------------- | |||
FIND AUDIOBOX TEMPLATE | |||
--------------------------------- */ | |||
const start = | |||
wikitext.indexOf( | |||
'{{Audiobox_NDrops|' | |||
); | |||
if (start === -1) { | |||
quoteChanging = false; | |||
window.setTimeout( | |||
getRandomNectarDrop, | |||
250 | |||
); | |||
return; | |||
} | |||
const end = | |||
wikitext.indexOf( | |||
'}}', | |||
start | |||
); | |||
if (end === -1) { | |||
quoteChanging = false; | |||
window.setTimeout( | |||
getRandomNectarDrop, | |||
250 | |||
); | |||
return; | |||
} | |||
const template = | |||
wikitext.substring( | |||
start + | |||
'{{Audiobox_NDrops|'.length, | |||
end | |||
); | |||
/* --------------------------------- | |||
SPLIT TEMPLATE PARAMETERS | |||
--------------------------------- */ | |||
const parts = | |||
template.split('|'); | |||
if (parts.length < 5) { | |||
quoteChanging = false; | |||
window.setTimeout( | |||
getRandomNectarDrop, | |||
250 | |||
); | |||
return; | |||
} | |||
/* | |||
* Parameter structure: | |||
* | |||
* 0 = Nectar Drops from... | |||
* 1 = mp3 player | |||
* 2 = QUOTE | |||
* 3 = Vanisource page | |||
* 4 = source label | |||
*/ | |||
let quote = | |||
parts[2] | |||
.trim() | |||
.replace( | |||
/^["“]+/, | |||
'' | |||
) | |||
.replace( | |||
/["”]+$/, | |||
'' | |||
) | |||
.trim(); | |||
const vanisourcePage = | |||
parts[3] | |||
.trim() | |||
/* remove wiki-link brackets */ | |||
.replace(/^\[\[/, '') | |||
.replace(/\]\]$/, '') | |||
/* if link contains |display text, keep only target */ | |||
.split('|')[0] | |||
/* remove namespace prefix */ | |||
.replace(/^Vanisource:/i, '') | |||
/* remove any remaining stray brackets/braces */ | |||
.replace(/[\[\]\{\}]/g, '') | |||
.trim(); | |||
const sourceLabel = | |||
parts[4] | |||
.trim(); | |||
if (!quote) { | |||
quoteChanging = false; | |||
window.setTimeout( | |||
getRandomNectarDrop, | |||
250 | |||
); | |||
return; | |||
} | |||
/* --------------------------------- | |||
CLEAN SIMPLE WIKITEXT | |||
--------------------------------- */ | |||
quote = | |||
quote | |||
.replace( | |||
/'''?/g, | |||
'' | |||
) | |||
.replace( | |||
/\[\[([^|\]]+)\|([^\]]+)\]\]/g, | |||
'$2' | |||
) | |||
.replace( | |||
/\[\[([^\]]+)\]\]/g, | |||
'$1' | |||
) | |||
.replace( | |||
/\s+/g, | |||
' ' | |||
) | |||
.trim(); | |||
/* --------------------------------- | |||
SOURCE LINK | |||
--------------------------------- */ | |||
const href = | |||
'https://vanisource.org/wiki/' + | |||
vanisourcePage | |||
.replace(/ /g, '_') | |||
.split('/') | |||
.map(encodeURIComponent) | |||
.join('/'); | |||
/* --------------------------------- | |||
DISPLAY | |||
--------------------------------- */ | |||
currentPage = page.title; | |||
quoteText.classList.add( | |||
'is-changing' | |||
); | |||
quoteSource.classList.add( | |||
'is-changing' | |||
); | |||
window.setTimeout( | |||
function() { | |||
const cleanQuote = | |||
quote | |||
.replace( | |||
/\s*\(?\[\[Vanisource:.*$/i, | |||
'' | |||
) | |||
.trim(); | |||
quoteText.textContent = | |||
cleanQuote; | |||
quoteSource.innerHTML = | |||
''; | |||
const link = | |||
document.createElement('a'); | |||
link.href = | |||
href; | |||
let cleanSource = page.title | |||
.replace(/^\d{6}[a-z]?\s*-\s*/i, '') | |||
.replace( | |||
/\s*-\s*Srila Prabhupada Speaks a Nectar Drop in\s*/i, | |||
', ' | |||
) | |||
.trim(); | |||
link.textContent = | |||
cleanSource; | |||
quoteSource.appendChild( | |||
link | |||
); | |||
/* | |||
* Reveal only after the first | |||
* random quote is ready. | |||
*/ | |||
quoteWrap.style.visibility = | |||
'visible'; | |||
requestAnimationFrame( | |||
function() { | |||
quoteText.classList.remove( | |||
'is-changing' | |||
); | |||
quoteSource.classList.remove( | |||
'is-changing' | |||
); | |||
quoteChanging = false; | |||
} | |||
); | |||
}, | |||
450 | |||
); | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Random Nectar Drop failed:', | |||
error | |||
); | |||
quoteChanging = false; | |||
}); | |||
} | |||
/* ----------------------------------------- | |||
START | |||
----------------------------------------- */ | |||
loadNectarPages() | |||
.then(function() { | |||
if (nectarPages.length) { | |||
getRandomNectarDrop(); | |||
window.setInterval( | |||
getRandomNectarDrop, | |||
9000 | |||
); | |||
} | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Nectar Drop category failed:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
/* Vani Treasure (Featured) */ | |||
/* ========================================================== | |||
HOMEPAGE — RANDOM SHORT | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const shortCard = | |||
document.getElementById('vh-random-short'); | |||
if (shortCard) { | |||
const API = | |||
'https://vanipedia.org/w/api.php'; | |||
const SHORTS_PAGE = | |||
'VanimediaMayapur YouTube SHORTS List'; | |||
let shortItems = []; | |||
let currentShort = -1; | |||
/* -------------------------------------------------- | |||
LOAD SHORTS LIST | |||
-------------------------------------------------- */ | |||
function loadShorts() { | |||
const url = | |||
API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(SHORTS_PAGE) + | |||
'&prop=wikitext'; | |||
fetch(url) | |||
.then(function(response) { | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.wikitext || | |||
!data.parse.wikitext['*'] | |||
) { | |||
throw new Error( | |||
'Shorts wikitext not found' | |||
); | |||
} | |||
const wiki = | |||
data.parse.wikitext['*']; | |||
/* | |||
* Example: | |||
* | |||
* File:Short_1139.png | |||
* |1139 - 21 August | |||
* |link=https://www.youtube.com/shorts/_Wumpw8SAYs | |||
*/ | |||
const regex = | |||
/File:(Short_(\d+)\.(?:png|jpg|jpeg|webp))\s*\|\s*(\d+)\s*-\s*([^|\n]+?)\s*\|\s*link=(https:\/\/www\.youtube\.com\/shorts\/[^\s|\n]+)/gi; | |||
let match; | |||
while ( | |||
(match = regex.exec(wiki)) !== null | |||
) { | |||
shortItems.push({ | |||
file: match[1], | |||
number: match[3], | |||
date: match[4].trim(), | |||
youtube: match[5].trim() | |||
}); | |||
} | |||
console.log( | |||
'Homepage Shorts loaded:', | |||
shortItems.length | |||
); | |||
if (shortItems.length) { | |||
showRandomShort(); | |||
} | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Shorts failed:', | |||
error | |||
); | |||
}); | |||
} | |||
/* -------------------------------------------------- | |||
GET ACTUAL THUMBNAIL URL | |||
-------------------------------------------------- */ | |||
function getShortThumbnail(fileName) { | |||
const url = | |||
API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&prop=imageinfo' + | |||
'&iiprop=url' + | |||
'&iiurlwidth=400' + | |||
'&titles=' + | |||
encodeURIComponent( | |||
'File:' + fileName | |||
); | |||
return fetch(url) | |||
.then(function(response) { | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.query || | |||
!data.query.pages | |||
) { | |||
return ''; | |||
} | |||
const pages = | |||
Object.values( | |||
data.query.pages | |||
); | |||
if ( | |||
!pages.length || | |||
!pages[0].imageinfo || | |||
!pages[0].imageinfo.length | |||
) { | |||
return ''; | |||
} | |||
const info = | |||
pages[0].imageinfo[0]; | |||
return ( | |||
info.thumburl || | |||
info.url || | |||
'' | |||
); | |||
}); | |||
} | |||
/* -------------------------------------------------- | |||
DISPLAY RANDOM SHORT | |||
-------------------------------------------------- */ | |||
function showRandomShort() { | |||
if (!shortItems.length) { | |||
return; | |||
} | |||
let index; | |||
do { | |||
index = | |||
Math.floor( | |||
Math.random() * | |||
shortItems.length | |||
); | |||
} | |||
while ( | |||
shortItems.length > 1 && | |||
index === currentShort | |||
); | |||
currentShort = index; | |||
const item = | |||
shortItems[index]; | |||
const media = | |||
shortCard.querySelector( | |||
'.vh-feature-media' | |||
); | |||
const copy = | |||
shortCard.querySelector( | |||
'.vh-feature-copy' | |||
); | |||
if (!media || !copy) { | |||
return; | |||
} | |||
/* ---------- thumbnail ---------- */ | |||
getShortThumbnail(item.file) | |||
.then(function(imageUrl) { | |||
if (!imageUrl) { | |||
return; | |||
} | |||
const preload = new Image(); | |||
preload.onload = function() { | |||
media.style.backgroundImage = | |||
'url("' + imageUrl + '")'; | |||
}; | |||
preload.src = imageUrl; | |||
}); | |||
/* ---------- text ---------- */ | |||
copy.innerHTML = ''; | |||
const type = | |||
document.createElement('small'); | |||
type.textContent = | |||
'SHORT'; | |||
const title = | |||
document.createElement('strong'); | |||
title.textContent = | |||
'Loading title…'; | |||
copy.appendChild(type); | |||
copy.appendChild(title); | |||
getShortTitle(item.youtube) | |||
.then(function(actualTitle) { | |||
/* | |||
* Make sure this card has not already | |||
* rotated to another Short. | |||
*/ | |||
if ( | |||
shortItems[currentShort] !== item | |||
) { | |||
return; | |||
} | |||
let cleanTitle = actualTitle | |||
.replace(/\s*-\s*Prabhupada.*$/i, '') | |||
.trim(); | |||
title.textContent = | |||
cleanTitle || | |||
item.number; | |||
}); | |||
/* ---------- click ---------- */ | |||
shortCard.onclick = | |||
function() { | |||
window.open( | |||
item.youtube, | |||
'_blank', | |||
'noopener' | |||
); | |||
}; | |||
} | |||
/* -------------------------------------------------- | |||
START | |||
-------------------------------------------------- */ | |||
loadShorts(); | |||
/* | |||
* Temporary individual rotation. | |||
* | |||
* Later we'll replace this with the common | |||
* staggered controller for all four cards. | |||
*/ | |||
window.setInterval( | |||
function() { | |||
if (shortItems.length) { | |||
showRandomShort(); | |||
} | |||
}, | |||
10500 | |||
); | |||
} | |||
} | |||
/* ========================================================== | |||
HOMEPAGE — RANDOM VIDEO | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const videoCard = | |||
document.getElementById('vh-random-video'); | |||
if (videoCard) { | |||
const API = | |||
'https://vanipedia.org/w/api.php'; | |||
const CATEGORY = | |||
'Category:1080 English Pages with Videos'; | |||
let videoPages = []; | |||
let currentVideo = ''; | |||
/* -------------------------------------------------- | |||
LOAD VIDEO PAGES | |||
-------------------------------------------------- */ | |||
function loadVideoPages(cmcontinue) { | |||
let url = | |||
API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent(CATEGORY) + | |||
'&cmnamespace=0' + | |||
'&cmtype=page' + | |||
'&cmlimit=max'; | |||
if (cmcontinue) { | |||
url += | |||
'&cmcontinue=' + | |||
encodeURIComponent(cmcontinue); | |||
} | |||
return fetch(url) | |||
.then(function(response) { | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
data.query && | |||
data.query.categorymembers | |||
) { | |||
data.query.categorymembers | |||
.forEach(function(page) { | |||
if ( | |||
/^Prabhupada \d{4}\s*-/i | |||
.test(page.title) | |||
) { | |||
videoPages.push(page); | |||
} | |||
}); | |||
} | |||
if ( | |||
data.continue && | |||
data.continue.cmcontinue | |||
) { | |||
return loadVideoPages( | |||
data.continue.cmcontinue | |||
); | |||
} | |||
}); | |||
} | |||
/* -------------------------------------------------- | |||
CHOOSE RANDOM VIDEO | |||
-------------------------------------------------- */ | |||
function chooseRandomVideoPage() { | |||
if (!videoPages.length) { | |||
return null; | |||
} | |||
let page; | |||
do { | |||
page = | |||
videoPages[ | |||
Math.floor( | |||
Math.random() * | |||
videoPages.length | |||
) | |||
]; | |||
} while ( | |||
videoPages.length > 1 && | |||
page.title === currentVideo | |||
); | |||
return page; | |||
} | |||
/* -------------------------------------------------- | |||
CLEAN TITLE | |||
-------------------------------------------------- */ | |||
function getVideoDisplayTitle(pageTitle) { | |||
return pageTitle | |||
.replace( | |||
/^Prabhupada\s+\d{4}\s*-\s*/i, | |||
'' | |||
) | |||
.trim(); | |||
} | |||
/* -------------------------------------------------- | |||
VANIPEDIA PAGE URL | |||
-------------------------------------------------- */ | |||
function getVideoPageUrl(pageTitle) { | |||
return ( | |||
'https://vanipedia.org/wiki/' + | |||
encodeURIComponent( | |||
pageTitle.replace(/ /g, '_') | |||
) | |||
.replace(/%2F/g, '/') | |||
.replace(/%3A/g, ':') | |||
); | |||
} | |||
/* -------------------------------------------------- | |||
GET YOUTUBE ID FROM PAGE WIKITEXT | |||
-------------------------------------------------- */ | |||
function loadVideoInfo(page) { | |||
const url = | |||
API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent(page.title) + | |||
'&prop=wikitext'; | |||
return fetch(url) | |||
.then(function(response) { | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.wikitext || | |||
!data.parse.wikitext['*'] | |||
) { | |||
return null; | |||
} | |||
const wiki = | |||
data.parse.wikitext['*']; | |||
/* | |||
* Example: | |||
* {{youtube_right|rEc1U_lsJ7s|Expand to Ten Million...}} | |||
*/ | |||
const match = | |||
wiki.match( | |||
/\{\{youtube_right\s*\|\s*([A-Za-z0-9_-]{11})\s*\|/i | |||
); | |||
const videoId = | |||
match | |||
? match[1] | |||
: ''; | |||
const thumbnailUrl = | |||
videoId | |||
? 'https://i.ytimg.com/vi/' + | |||
videoId + | |||
'/hqdefault.jpg' | |||
: ''; | |||
return { | |||
pageUrl: | |||
getVideoPageUrl( | |||
page.title | |||
), | |||
videoId: | |||
videoId, | |||
thumbnailUrl: | |||
thumbnailUrl | |||
}; | |||
}); | |||
} | |||
/* -------------------------------------------------- | |||
SHOW RANDOM VIDEO | |||
-------------------------------------------------- */ | |||
function showRandomVideo() { | |||
const page = | |||
chooseRandomVideoPage(); | |||
if (!page) { | |||
return; | |||
} | |||
currentVideo = | |||
page.title; | |||
const media = | |||
videoCard.querySelector( | |||
'.vh-feature-media' | |||
); | |||
const copy = | |||
videoCard.querySelector( | |||
'.vh-feature-copy' | |||
); | |||
if (!media || !copy) { | |||
return; | |||
} | |||
/* ---------- title ---------- */ | |||
copy.innerHTML = ''; | |||
const type = | |||
document.createElement('small'); | |||
type.textContent = | |||
'VIDEO'; | |||
const title = | |||
document.createElement('strong'); | |||
title.textContent = | |||
getVideoDisplayTitle( | |||
page.title | |||
); | |||
copy.appendChild(type); | |||
copy.appendChild(title); | |||
/* ---------- thumbnail + click ---------- */ | |||
loadVideoInfo(page) | |||
.then(function(info) { | |||
if ( | |||
!info || | |||
currentVideo !== page.title | |||
) { | |||
return; | |||
} | |||
if (info.thumbnailUrl) { | |||
const preload = | |||
new Image(); | |||
preload.onload = | |||
function() { | |||
if ( | |||
currentVideo === | |||
page.title | |||
) { | |||
media.style.backgroundImage = | |||
'url("' + | |||
info.thumbnailUrl + | |||
'")'; | |||
} | |||
}; | |||
preload.src = | |||
info.thumbnailUrl; | |||
} | |||
videoCard.onclick = | |||
function() { | |||
const target = | |||
info.videoId | |||
? 'https://www.youtube.com/watch?v=' + | |||
info.videoId | |||
: info.pageUrl; | |||
window.open( | |||
target, | |||
'_blank', | |||
'noopener' | |||
); | |||
}; | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Video failed:', | |||
error | |||
); | |||
}); | |||
} | |||
/* -------------------------------------------------- | |||
START | |||
-------------------------------------------------- */ | |||
loadVideoPages() | |||
.then(function() { | |||
console.log( | |||
'Homepage Video pages loaded:', | |||
videoPages.length | |||
); | |||
if (videoPages.length) { | |||
showRandomVideo(); | |||
window.setInterval( | |||
showRandomVideo, | |||
11500 | |||
); | |||
} | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Video category failed:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
/* ========================================================== | |||
HOMEPAGE — RANDOM IMAGE QUOTE | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const imageQuoteCard = | |||
document.getElementById('vh-random-imagequote'); | |||
if (imageQuoteCard) { | |||
const IMAGE_API = | |||
'https://vanimedia.org/w/api.php'; | |||
const MONTH_CATEGORIES = [ | |||
'Category:January Quotes with Images', | |||
'Category:February Quotes with Images', | |||
'Category:March Quotes with Images', | |||
'Category:April Quotes with Images', | |||
'Category:May Quotes with Images', | |||
'Category:June Quotes with Images', | |||
'Category:July Quotes with Images', | |||
'Category:August Quotes with Images', | |||
'Category:September Quotes with Images', | |||
'Category:October Quotes with Images', | |||
'Category:November Quotes with Images', | |||
'Category:December Quotes with Images' | |||
]; | |||
let imageQuoteFiles = []; | |||
let currentImageQuote = ''; | |||
/* ====================================================== | |||
LOAD ONE MONTH CATEGORY | |||
====================================================== */ | |||
function loadImageQuoteCategory( | |||
category, | |||
cmcontinue | |||
) { | |||
let url = | |||
IMAGE_API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent(category) + | |||
'&cmnamespace=6' + | |||
'&cmtype=file' + | |||
'&cmlimit=max'; | |||
if (cmcontinue) { | |||
url += | |||
'&cmcontinue=' + | |||
encodeURIComponent(cmcontinue); | |||
} | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'Image Quote category HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
data.query && | |||
data.query.categorymembers | |||
) { | |||
data.query.categorymembers | |||
.forEach(function(file) { | |||
if (file.title) { | |||
imageQuoteFiles.push( | |||
file | |||
); | |||
} | |||
}); | |||
} | |||
if ( | |||
data.continue && | |||
data.continue.cmcontinue | |||
) { | |||
return loadImageQuoteCategory( | |||
category, | |||
data.continue.cmcontinue | |||
); | |||
} | |||
}); | |||
} | |||
/* ====================================================== | |||
LOAD ALL MONTHS | |||
====================================================== */ | |||
function loadAllImageQuotes() { | |||
const jobs = | |||
MONTH_CATEGORIES.map( | |||
function(category) { | |||
return loadImageQuoteCategory( | |||
category | |||
); | |||
} | |||
); | |||
return Promise.all(jobs); | |||
} | |||
/* ====================================================== | |||
RANDOM FILE | |||
====================================================== */ | |||
function chooseRandomImageQuote() { | |||
if (!imageQuoteFiles.length) { | |||
return null; | |||
} | |||
let file; | |||
do { | |||
file = | |||
imageQuoteFiles[ | |||
Math.floor( | |||
Math.random() * | |||
imageQuoteFiles.length | |||
) | |||
]; | |||
} while ( | |||
imageQuoteFiles.length > 1 && | |||
file.title === currentImageQuote | |||
); | |||
return file; | |||
} | |||
/* ====================================================== | |||
GET THUMBNAIL + FULL IMAGE | |||
====================================================== */ | |||
function getImageQuoteInfo(file) { | |||
const url = | |||
IMAGE_API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&prop=imageinfo' + | |||
'&iiprop=url' + | |||
'&iiurlwidth=500' + | |||
'&titles=' + | |||
encodeURIComponent( | |||
file.title | |||
); | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'Image Quote image HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.query || | |||
!data.query.pages | |||
) { | |||
return null; | |||
} | |||
const page = | |||
Object.values( | |||
data.query.pages | |||
)[0]; | |||
if ( | |||
!page || | |||
!page.imageinfo || | |||
!page.imageinfo.length | |||
) { | |||
return null; | |||
} | |||
const info = | |||
page.imageinfo[0]; | |||
const thumbnailUrl = | |||
info.thumburl || | |||
info.url || | |||
''; | |||
const fullUrl = | |||
info.url || | |||
thumbnailUrl; | |||
const pageUrl = | |||
'https://vanimedia.org/wiki/' + | |||
encodeURIComponent( | |||
file.title.replace( | |||
/ /g, | |||
'_' | |||
) | |||
) | |||
.replace(/%2F/g, '/') | |||
.replace(/%3A/g, ':'); | |||
return { | |||
thumbnailUrl: | |||
thumbnailUrl, | |||
fullUrl: | |||
fullUrl, | |||
pageUrl: | |||
pageUrl | |||
}; | |||
}); | |||
} | |||
/* ====================================================== | |||
FILENAME → HUMAN DATE | |||
072866 → July 28, 1966 | |||
====================================================== */ | |||
function getImageQuoteTitle( | |||
fileTitle | |||
) { | |||
const clean = | |||
fileTitle | |||
.replace( | |||
/^File:/i, | |||
'' | |||
) | |||
.replace( | |||
/\.(jpg|jpeg|png|webp)$/i, | |||
'' | |||
) | |||
.replace( | |||
/\s*Image-quote$/i, | |||
'' | |||
) | |||
.trim(); | |||
const match = | |||
clean.match( | |||
/^(\d{2})(\d{2})(\d{2})/ | |||
); | |||
if (!match) { | |||
return clean; | |||
} | |||
const month = | |||
parseInt( | |||
match[1], | |||
10 | |||
); | |||
const day = | |||
parseInt( | |||
match[2], | |||
10 | |||
); | |||
const year = | |||
1900 + | |||
parseInt( | |||
match[3], | |||
10 | |||
); | |||
const months = [ | |||
'January', | |||
'February', | |||
'March', | |||
'April', | |||
'May', | |||
'June', | |||
'July', | |||
'August', | |||
'September', | |||
'October', | |||
'November', | |||
'December' | |||
]; | |||
if ( | |||
month < 1 || | |||
month > 12 || | |||
day < 1 || | |||
day > 31 | |||
) { | |||
return clean; | |||
} | |||
return ( | |||
months[month - 1] + | |||
' ' + | |||
day + | |||
', ' + | |||
year | |||
); | |||
} | |||
/* ====================================================== | |||
LIGHTBOX | |||
====================================================== */ | |||
function openImageQuoteLightbox( | |||
imageUrl | |||
) { | |||
let lightbox = | |||
document.getElementById( | |||
'vh-imagequote-lightbox' | |||
); | |||
if (!lightbox) { | |||
lightbox = | |||
document.createElement( | |||
'div' | |||
); | |||
lightbox.id = | |||
'vh-imagequote-lightbox'; | |||
lightbox.innerHTML = | |||
'<button ' + | |||
'class="vh-imagequote-lightbox-close" ' + | |||
'type="button" ' + | |||
'aria-label="Close">×</button>' + | |||
'<img ' + | |||
'alt="Śrīla Prabhupāda Image Quote">'; | |||
document.body.appendChild( | |||
lightbox | |||
); | |||
lightbox.addEventListener( | |||
'mouseleave', | |||
function() { | |||
lightbox.classList.remove( | |||
'is-open' | |||
); | |||
} | |||
); | |||
lightbox.addEventListener( | |||
'click', | |||
function(event) { | |||
if ( | |||
event.target === | |||
lightbox || | |||
event.target | |||
.classList | |||
.contains( | |||
'vh-imagequote-lightbox-close' | |||
) | |||
) { | |||
lightbox.classList | |||
.remove( | |||
'is-open' | |||
); | |||
} | |||
} | |||
); | |||
document.addEventListener( | |||
'keydown', | |||
function(event) { | |||
if ( | |||
event.key === | |||
'Escape' | |||
) { | |||
lightbox.classList | |||
.remove( | |||
'is-open' | |||
); | |||
} | |||
} | |||
); | |||
} | |||
const image = | |||
lightbox.querySelector( | |||
'img' | |||
); | |||
image.src = | |||
imageUrl; | |||
const cardRect = | |||
imageQuoteCard.getBoundingClientRect(); | |||
lightbox.classList.add( | |||
'is-open' | |||
); | |||
requestAnimationFrame(function() { | |||
const popupRect = | |||
lightbox.getBoundingClientRect(); | |||
/* | |||
* Place popup immediately to the | |||
* right of the Image Quote card. | |||
*/ | |||
let left = | |||
cardRect.right - 240; | |||
let top = | |||
cardRect.top - 220; | |||
/* | |||
* If there is not enough room | |||
* on the right, place it above. | |||
*/ | |||
if ( | |||
left + | |||
popupRect.width > | |||
window.innerWidth - 10 | |||
) { | |||
left = | |||
cardRect.right - | |||
popupRect.width; | |||
top = | |||
cardRect.top - | |||
popupRect.height - | |||
12; | |||
} | |||
/* | |||
* Keep inside viewport. | |||
*/ | |||
left = | |||
Math.max( | |||
10, | |||
Math.min( | |||
left, | |||
window.innerWidth - | |||
popupRect.width - | |||
10 | |||
) | |||
); | |||
top = | |||
Math.max( | |||
10, | |||
top | |||
); | |||
lightbox.style.left = | |||
left + 'px'; | |||
lightbox.style.top = | |||
top + 'px'; | |||
}); | |||
} | |||
/* ====================================================== | |||
DISPLAY RANDOM IMAGE QUOTE | |||
====================================================== */ | |||
function showRandomImageQuote() { | |||
const file = | |||
chooseRandomImageQuote(); | |||
if (!file) { | |||
return; | |||
} | |||
currentImageQuote = | |||
file.title; | |||
const media = | |||
imageQuoteCard.querySelector( | |||
'.vh-feature-media' | |||
); | |||
const copy = | |||
imageQuoteCard.querySelector( | |||
'.vh-feature-copy' | |||
); | |||
if (!media || !copy) { | |||
return; | |||
} | |||
/* | |||
* Card text | |||
*/ | |||
copy.innerHTML = ''; | |||
const type = | |||
document.createElement( | |||
'small' | |||
); | |||
type.textContent = | |||
'IMAGE QUOTE'; | |||
const title = | |||
document.createElement( | |||
'strong' | |||
); | |||
title.textContent = | |||
getImageQuoteTitle( | |||
file.title | |||
); | |||
copy.appendChild( | |||
type | |||
); | |||
copy.appendChild( | |||
title | |||
); | |||
/* | |||
* Load image | |||
*/ | |||
getImageQuoteInfo( | |||
file | |||
) | |||
.then(function(info) { | |||
if ( | |||
!info || | |||
currentImageQuote !== | |||
file.title | |||
) { | |||
return; | |||
} | |||
if ( | |||
info.thumbnailUrl | |||
) { | |||
const preload = | |||
new Image(); | |||
preload.onload = | |||
function() { | |||
if ( | |||
currentImageQuote === | |||
file.title | |||
) { | |||
media.style | |||
.backgroundImage = | |||
'url("' + | |||
info.thumbnailUrl + | |||
'")'; | |||
} | |||
}; | |||
preload.src = | |||
info.thumbnailUrl; | |||
} | |||
/* | |||
* Open full image | |||
* in lightbox | |||
*/ | |||
imageQuoteCard.onclick = | |||
function() { | |||
if ( | |||
info.fullUrl | |||
) { | |||
openImageQuoteLightbox( | |||
info.fullUrl | |||
); | |||
} | |||
}; | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Image Quote failed:', | |||
error | |||
); | |||
}); | |||
} | |||
/* ====================================================== | |||
START | |||
====================================================== */ | |||
loadAllImageQuotes() | |||
.then(function() { | |||
console.log( | |||
'Homepage Image Quotes loaded:', | |||
imageQuoteFiles.length | |||
); | |||
if ( | |||
imageQuoteFiles.length | |||
) { | |||
showRandomImageQuote(); | |||
window.setInterval( | |||
showRandomImageQuote, | |||
9800 | |||
); | |||
} | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Image Quote categories failed:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
/* ========================================================== | |||
HOMEPAGE — RANDOM NECTAR DROP | |||
========================================================== */ | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
const nectarCard = | |||
document.getElementById('vh-random-nectar'); | |||
if (nectarCard) { | |||
const NECTAR_API = | |||
'https://vanipedia.org/w/api.php'; | |||
const NECTAR_CATEGORY = | |||
'Category:Nectar Drops from Srila Prabhupada'; | |||
let nectarPages = []; | |||
let currentNectar = ''; | |||
let nectarAudio = null; | |||
let nectarLocked = false; | |||
const audioArea = | |||
nectarCard.querySelector( | |||
'.vh-feature-audio' | |||
); | |||
const audioIcon = | |||
nectarCard.querySelector( | |||
'.vh-feature-audio-icon' | |||
); | |||
const nectarWave = | |||
document.createElement( | |||
'div' | |||
); | |||
nectarWave.className = | |||
'vh-nectar-wave'; | |||
nectarWave.innerHTML = | |||
'<span></span>' + | |||
'<span></span>' + | |||
'<span></span>' + | |||
'<span></span>' + | |||
'<span></span>' + | |||
'<span></span>' + | |||
'<span></span>'; | |||
audioArea.appendChild( | |||
nectarWave | |||
); | |||
const copy = | |||
nectarCard.querySelector( | |||
'.vh-feature-copy' | |||
); | |||
/* ====================================================== | |||
LOAD ALL NECTAR DROP PAGES | |||
====================================================== */ | |||
function loadNectarPages( | |||
cmcontinue | |||
) { | |||
let url = | |||
NECTAR_API + | |||
'?action=query' + | |||
'&format=json' + | |||
'&list=categorymembers' + | |||
'&cmtitle=' + | |||
encodeURIComponent( | |||
NECTAR_CATEGORY | |||
) + | |||
'&cmnamespace=0' + | |||
'&cmtype=page' + | |||
'&cmlimit=max'; | |||
if (cmcontinue) { | |||
url += | |||
'&cmcontinue=' + | |||
encodeURIComponent( | |||
cmcontinue | |||
); | |||
} | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'Nectar category HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
data.query && | |||
data.query.categorymembers | |||
) { | |||
data.query.categorymembers | |||
.forEach(function(page) { | |||
if (page.title) { | |||
nectarPages.push( | |||
page | |||
); | |||
} | |||
}); | |||
} | |||
if ( | |||
data.continue && | |||
data.continue.cmcontinue | |||
) { | |||
return loadNectarPages( | |||
data.continue.cmcontinue | |||
); | |||
} | |||
}); | |||
} | |||
/* ====================================================== | |||
CHOOSE RANDOM NECTAR DROP | |||
====================================================== */ | |||
function chooseRandomNectar() { | |||
if (!nectarPages.length) { | |||
return null; | |||
} | |||
let page; | |||
do { | |||
page = | |||
nectarPages[ | |||
Math.floor( | |||
Math.random() * | |||
nectarPages.length | |||
) | |||
]; | |||
} while ( | |||
nectarPages.length > 1 && | |||
page.title === currentNectar | |||
); | |||
return page; | |||
} | |||
/* ====================================================== | |||
CREATE SHORT MEANINGFUL TITLE FROM QUOTE | |||
====================================================== */ | |||
function getNectarShortText( | |||
quote | |||
) { | |||
if (!quote) { | |||
return 'Śrīla Prabhupāda speaks'; | |||
} | |||
let text = | |||
quote | |||
.replace( | |||
/^["“”]+|["“”]+$/g, | |||
'' | |||
) | |||
.replace( | |||
/\s+/g, | |||
' ' | |||
) | |||
.trim(); | |||
/* | |||
* Prefer first sentence when it is | |||
* reasonably short. | |||
*/ | |||
const sentenceMatch = | |||
text.match( | |||
/^(.+?[.!?])(?:\s|$)/ | |||
); | |||
if ( | |||
sentenceMatch && | |||
sentenceMatch[1].length <= 85 | |||
) { | |||
return sentenceMatch[1]; | |||
} | |||
/* | |||
* Otherwise take approximately | |||
* the first 70 characters, | |||
* ending at a full word. | |||
*/ | |||
if (text.length > 72) { | |||
text = | |||
text.substring( | |||
0, | |||
72 | |||
); | |||
const lastSpace = | |||
text.lastIndexOf( | |||
' ' | |||
); | |||
if (lastSpace > 45) { | |||
text = | |||
text.substring( | |||
0, | |||
lastSpace | |||
); | |||
} | |||
text += '…'; | |||
} | |||
return text; | |||
} | |||
/* ====================================================== | |||
GET QUOTE TEXT + AUDIO URL | |||
====================================================== */ | |||
function getNectarInfo( | |||
page | |||
) { | |||
const url = | |||
NECTAR_API + | |||
'?action=parse' + | |||
'&format=json' + | |||
'&page=' + | |||
encodeURIComponent( | |||
page.title | |||
) + | |||
'&prop=text'; | |||
return fetch(url) | |||
.then(function(response) { | |||
if (!response.ok) { | |||
throw new Error( | |||
'Nectar page HTTP ' + | |||
response.status | |||
); | |||
} | |||
return response.json(); | |||
}) | |||
.then(function(data) { | |||
if ( | |||
!data.parse || | |||
!data.parse.text || | |||
!data.parse.text['*'] | |||
) { | |||
return null; | |||
} | |||
const wrapper = | |||
document.createElement( | |||
'div' | |||
); | |||
wrapper.innerHTML = | |||
data.parse.text['*']; | |||
/* | |||
* Quote | |||
*/ | |||
const quoteElement = | |||
wrapper.querySelector( | |||
'.nectardroptext' | |||
); | |||
const quote = | |||
quoteElement | |||
? quoteElement | |||
.textContent | |||
.replace( | |||
/\s+/g, | |||
' ' | |||
) | |||
.trim() | |||
: ''; | |||
/* | |||
* MP3 source | |||
*/ | |||
const source = | |||
wrapper.querySelector( | |||
'audio source' | |||
); | |||
const audioUrl = | |||
source | |||
? source.getAttribute( | |||
'src' | |||
) | |||
: ''; | |||
return { | |||
quote: | |||
quote, | |||
audioUrl: | |||
audioUrl | |||
}; | |||
}); | |||
} | |||
/* ====================================================== | |||
RESET PLAY BUTTON | |||
====================================================== */ | |||
function resetNectarPlayButton() { | |||
if (audioIcon) { | |||
audioIcon.textContent = | |||
'▶'; | |||
} | |||
} | |||
/* ====================================================== | |||
PREPARE AUDIO | |||
====================================================== */ | |||
function prepareNectarAudio( | |||
audioUrl | |||
) { | |||
/* | |||
* Dispose of previous audio object. | |||
*/ | |||
if (nectarAudio) { | |||
nectarAudio.pause(); | |||
nectarAudio.src = ''; | |||
nectarAudio = null; | |||
} | |||
resetNectarPlayButton(); | |||
if (!audioUrl) { | |||
return; | |||
} | |||
nectarAudio = | |||
new Audio( | |||
audioUrl | |||
); | |||
nectarAudio.preload = | |||
'metadata'; | |||
/* | |||
* Audio finished naturally. | |||
*/ | |||
/* | |||
* Audio finished naturally. | |||
*/ | |||
nectarAudio.addEventListener( | |||
'ended', | |||
function() { | |||
resetNectarPlayButton(); | |||
nectarWave.classList.remove( | |||
'is-playing' | |||
); | |||
nectarLocked = | |||
false; | |||
} | |||
); | |||
/* | |||
* Audio starts / resumes. | |||
*/ | |||
nectarAudio.addEventListener( | |||
'play', | |||
function() { | |||
if (audioIcon) { | |||
audioIcon.textContent = | |||
'❚❚'; | |||
} | |||
nectarWave.classList.add( | |||
'is-playing' | |||
); | |||
} | |||
); | |||
/* | |||
* Audio pauses. | |||
*/ | |||
nectarAudio.addEventListener( | |||
'pause', | |||
function() { | |||
if ( | |||
nectarAudio && | |||
!nectarAudio.ended && | |||
audioIcon | |||
) { | |||
audioIcon.textContent = | |||
'▶'; | |||
} | |||
nectarWave.classList.remove( | |||
'is-playing' | |||
); | |||
} | |||
); | |||
} | |||
/* ====================================================== | |||
PLAY / PAUSE | |||
====================================================== */ | |||
function toggleNectarAudio( | |||
event | |||
) { | |||
if (event) { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
} | |||
if (!nectarAudio) { | |||
return; | |||
} | |||
/* | |||
* Playing → pause | |||
*/ | |||
if (!nectarAudio.paused) { | |||
nectarAudio.pause(); | |||
nectarLocked = | |||
false; | |||
return; | |||
} | |||
/* | |||
* Paused → play/resume | |||
*/ | |||
nectarLocked = | |||
true; | |||
nectarAudio | |||
.play() | |||
.catch(function(error) { | |||
console.error( | |||
'Nectar Drop audio failed:', | |||
error | |||
); | |||
}); | |||
} | |||
/* ====================================================== | |||
DISPLAY RANDOM NECTAR DROP | |||
====================================================== */ | |||
function showRandomNectar() { | |||
/* | |||
* Never change the card while | |||
* user is listening or has paused it. | |||
*/ | |||
if (nectarLocked) { | |||
return; | |||
} | |||
const page = | |||
chooseRandomNectar(); | |||
if (!page) { | |||
return; | |||
} | |||
currentNectar = | |||
page.title; | |||
if (!copy) { | |||
return; | |||
} | |||
copy.innerHTML = ''; | |||
const type = | |||
document.createElement( | |||
'small' | |||
); | |||
type.textContent = | |||
'NECTAR DROP'; | |||
const title = | |||
document.createElement( | |||
'strong' | |||
); | |||
title.textContent = | |||
''; | |||
copy.appendChild( | |||
type | |||
); | |||
copy.appendChild( | |||
title | |||
); | |||
getNectarInfo( | |||
page | |||
) | |||
.then(function(info) { | |||
if ( | |||
!info || | |||
currentNectar !== | |||
page.title | |||
) { | |||
return; | |||
} | |||
/* | |||
* Display meaningful words | |||
* directly from Nectar Drop. | |||
*/ | |||
title.textContent = | |||
getNectarShortText( | |||
info.quote | |||
); | |||
/* | |||
* Prepare MP3. | |||
*/ | |||
prepareNectarAudio( | |||
info.audioUrl | |||
); | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Nectar Drop failed:', | |||
error | |||
); | |||
title.textContent = | |||
'Śrīla Prabhupāda speaks'; | |||
}); | |||
} | |||
/* ====================================================== | |||
PLAY BUTTON CLICK | |||
====================================================== */ | |||
if (audioArea) { | |||
audioArea.addEventListener( | |||
'click', | |||
toggleNectarAudio | |||
); | |||
} | |||
/* ====================================================== | |||
START | |||
====================================================== */ | |||
loadNectarPages() | |||
.then(function() { | |||
console.log( | |||
'Homepage Nectar Drops loaded:', | |||
nectarPages.length | |||
); | |||
if ( | |||
nectarPages.length | |||
) { | |||
showRandomNectar(); | |||
window.setInterval( | |||
function() { | |||
if (!nectarLocked) { | |||
showRandomNectar(); | |||
} | |||
}, | |||
12700 | |||
); | |||
} | |||
}) | |||
.catch(function(error) { | |||
console.error( | |||
'Homepage Nectar category failed:', | |||
error | |||
); | |||
}); | |||
} | |||
} | |||
const networkMission = | |||
document.getElementById('vh-network-mission'); | |||
if (networkMission) { | |||
networkMission.addEventListener( | |||
'click', | |||
function() { | |||
window.location.href = | |||
mw.util.getUrl('Vanipedia_Vision'); | |||
} | |||
); | |||
} | |||
/* CLOSE HOMEPAGE WRAPPER */ | |||
})(); | |||
/* ========================================================= | |||
VANIPEDIA FLIPBOOK POPUP | |||
========================================================= */ | |||
(function () { | |||
function createFlipbookOverlay() { | |||
if (document.getElementById('vani-flipbook-overlay')) { | |||
return; | |||
} | |||
var overlay = document.createElement('div'); | |||
overlay.id = 'vani-flipbook-overlay'; | |||
var modal = document.createElement('div'); | |||
modal.className = 'vani-flipbook-modal'; | |||
var closeButton = document.createElement('button'); | |||
closeButton.className = 'vani-flipbook-close'; | |||
closeButton.type = 'button'; | |||
closeButton.setAttribute('aria-label', 'Close flipbook'); | |||
closeButton.innerHTML = '×'; | |||
var frameHolder = document.createElement('div'); | |||
frameHolder.id = 'vani-flipbook-frame-holder'; | |||
modal.appendChild(closeButton); | |||
modal.appendChild(frameHolder); | |||
overlay.appendChild(modal); | |||
document.body.appendChild(overlay); | |||
closeButton.addEventListener('click', function () { | |||
closeFlipbook(); | |||
}); | |||
overlay.addEventListener('click', function (event) { | |||
if (event.target === overlay) { | |||
closeFlipbook(); | |||
} | |||
}); | |||
document.addEventListener('keydown', function (event) { | |||
if ( | |||
event.key === 'Escape' && | |||
overlay.classList.contains('is-open') | |||
) { | |||
closeFlipbook(); | |||
} | |||
}); | |||
} | |||
function openFlipbook(book) { | |||
createFlipbookOverlay(); | |||
var overlay = | |||
document.getElementById('vani-flipbook-overlay'); | |||
var holder = | |||
document.getElementById('vani-flipbook-frame-holder'); | |||
/* | |||
* FIRST make popup visible. | |||
*/ | |||
overlay.classList.add('is-open'); | |||
document.body.classList.add('vani-flipbook-open'); | |||
/* | |||
* Remove any previous iframe. | |||
*/ | |||
holder.innerHTML = ''; | |||
/* | |||
* Wait until browser has calculated | |||
* the real popup dimensions. | |||
*/ | |||
requestAnimationFrame(function () { | |||
requestAnimationFrame(function () { | |||
var frame = | |||
document.createElement('iframe'); | |||
frame.id = | |||
'vani-flipbook-frame'; | |||
frame.title = | |||
'Vanipedia Flipbook'; | |||
frame.setAttribute( | |||
'allowfullscreen', | |||
'true' | |||
); | |||
frame.src = | |||
'https://dev.vanipedia.org/flipbook/?book=' + | |||
encodeURIComponent(book); | |||
holder.appendChild(frame); | |||
}); | |||
}); | |||
} | |||
function closeFlipbook() { | |||
var overlay = | |||
document.getElementById('vani-flipbook-overlay'); | |||
var holder = | |||
document.getElementById('vani-flipbook-frame-holder'); | |||
if (!overlay) { | |||
return; | |||
} | |||
overlay.classList.remove('is-open'); | |||
document.body.classList.remove('vani-flipbook-open'); | |||
if (holder) { | |||
holder.innerHTML = ''; | |||
} | |||
} | |||
function initialiseFlipbooks() { | |||
var triggers = | |||
document.querySelectorAll( | |||
'.vani-flipbook-trigger' | |||
); | |||
triggers.forEach(function (trigger) { | |||
if ( | |||
trigger.getAttribute( | |||
'data-flipbook-ready' | |||
) === '1' | |||
) { | |||
return; | |||
} | |||
trigger.setAttribute( | |||
'data-flipbook-ready', | |||
'1' | |||
); | |||
trigger.addEventListener( | |||
'click', | |||
function (event) { | |||
event.preventDefault(); | |||
var book = | |||
trigger.getAttribute( | |||
'data-book' | |||
); | |||
if (book) { | |||
openFlipbook(book); | |||
} | |||
} | |||
); | |||
}); | |||
} | |||
createFlipbookOverlay(); | |||
initialiseFlipbooks(); | |||
if ( | |||
window.mw && | |||
mw.hook | |||
) { | |||
mw.hook( | |||
'wikipage.content' | |||
).add(function () { | |||
initialiseFlipbooks(); | |||
}); | |||
} | |||
})(); | |||
Revision as of 11:29, 16 September 2026
/* Any JavaScript here will be loaded for all users on every page load. */
if (mw.config.get('wgPageName' ) === 'Krishna_says_in_Bhagavad-gita') {
/* switch tooltip texts from content-div to label-div */
const v_labels = Array.from(document.getElementsByClassName("tabs-label"));
const v_content = Array.from(document.getElementsByClassName("tabs-content"));
for (var i = 0; i < v_labels.length; i++) {
v_labels[i].title = v_content[i].title;
v_content[i].title = "";
}
/* adjust wikitables */
var arr = Array.from(document.getElementsByClassName("wikitable"));
for (var t = 0; t < arr.length; t++) {
/* insert THEAD in wikitable */
var thead = document.createElement("thead");
thead.style.display = "block";
var wtbl = arr[t];
wtbl.insertBefore(thead, wtbl.firstChild);
/* move first row of TBODY to THEAD */
thead.appendChild(wtbl.rows[0]);
/* make TBODY scrollable */
var tbody = wtbl.tBodies[0];
tbody.style.display = "block";
tbody.style.overflowY = "auto";
tbody.style.maxHeight = "500px";
for (var r = 0; r < wtbl.rows.length; r++) {
var row = wtbl.rows[r];
/* adjust column widths and hide from-until columns */
row.cells[0].classList = "cw_0";
row.cells[1].classList = "cw_150";
row.cells[2].classList = "cw_500";
/* mark the text */
if (r == 0) continue;
var v_text = row.cells[2].textContent;
var v_html = ""; var v_from = 0;
var marks = JSON.parse(row.cells[0].textContent.replaceAll("O", "[").replaceAll("C", "]"));
for (var m = 0; m < marks.length; m++) {
v_html +=
v_text.substr(v_from,marks[m][0]-v_from) + "<mark>" +
v_text.substr(marks[m][0],marks[m][1]-marks[m][0]+1) + "</mark>";
v_from = marks[m][1]+1;
}
v_html += v_text.substr(v_from);
row.cells[2].innerHTML = v_html;
}
}
}
// install Google Search
// document.write('<script type="text/javascript" src="'
// + 'http://vanipedia.org/w/index.php?title=MediaWiki:GoogleSearch.js'
// + '&action=raw&ctype=text/javascript"></' + 'script>');
// install JQuery
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:JQuery.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install WikEd toolbar and Vanify
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vaniquotes.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install Replace
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:TextboxReplace.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install Unify
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Unify.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install Vanify
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vanify.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install Vani_suggest for diacritics auto-suggest pulldown on the search bar
//document.write('<script type="text/javascript" src="'
//+ 'http://vanipedia.org/w/index.php?title=MediaWiki:Vani_suggest.js'
//+ '&action=raw&ctype=text/javascript"></' + 'script>');
// install previous/next links for video categories in all languages
//document.write('<script type="text/javascript" src="/navcat/navcat.js"></' + 'script>');
// Buttons
//addOnloadHook(function() {
//if (document.editform) {
//addPortletLink("p-cactions", "javascript:Unify()", "unify", "ca-unify", "Convert Balarama fonts to Unicode", "");
// addPortletLink("p-cactions", "javascript:Vanify()", "vanify", "ca-vanify", "Corrects vani markup", "");
// addPortletLink('p-cactions', 'javascript:format()', 'format', 'ca-format', 'Format article', '');
//addPortletLink('p-cactions', 'javascript:wpTextboxReplace()', 'Replace', 'ca-replace', 'Replaces text in the edit window', 'R');
//}
//});
// end Buttons
// --- YouTube Random Shorts (Kasya) ---
$(document).ready(function() {
var $widget = $('.vani-shorts-viewport');
if ($widget.length) {
var $viewport = $widget;
var $titleFrame = $widget.find('.vani-shorts-title');
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: 'VanimediaMayapur YouTube SHORTS List',
rvprop: 'content',
format: 'json'
}).done(function(data) {
try {
var pages = data.query.pages;
var pageId = Object.keys(pages)[0];
if (pageId == "-1" || !pages[pageId].revisions) {
$viewport.text("Database list page not found.");
return;
}
// FIXED: Corrected MediaWiki revision array syntax lookup
var rawWikiText = pages[pageId].revisions[0]['*'];
var lines = rawWikiText.split('\n');
var validShorts = [];
for (var i = 0; i < lines.length; i++) {
var line = $.trim(lines[i]);
if (line.indexOf('File:') === 0 && line.toLowerCase().indexOf('link=') !== -1) {
validShorts.push(line);
}
}
if (validShorts.length === 0) {
$viewport.text("No gallery entries recognized.");
return;
}
var randomLine = validShorts[Math.floor(Math.random() * validShorts.length)];
var parts = randomLine.split('|');
// Reset defaults before parsing line elements
var rawUrl = "";
for (var j = 0; j < parts.length; j++) {
var part = $.trim(parts[j]);
if (part.toLowerCase().indexOf('link=') === 0) {
rawUrl = part.substring(5).trim();
}
}
// Clean all template formatting brackets, space characters, and hidden control markers
var urlCleaned = rawUrl.replace(/[\u200e\u200f\s\}\}\]]/g, '');
// Direct YouTube Alphanumeric Pattern Matcher
var idRegex = /(?:shorts\/|v=|embed\/|\/)([a-zA-Z0-9_-]{11})/;
var matchResults = urlCleaned.match(idRegex);
var videoId = "";
if (matchResults && matchResults[1]) {
// FIXED: Grabbed index 1 to pull the standalone 11-digit ID without any slashes
videoId = matchResults[1].trim();
}
if (videoId && videoId.length === 11) {
// FIXED: Isolated to inject ONLY the video player iframe at the correct 280px width
var embedUrl = "https://www.youtube.com/embed/" + videoId;
$viewport.html('<iframe width="280" height="225" src="' + embedUrl + '" frameborder="0" allowfullscreen style="border-radius: 8px; border: none;"></iframe>');
} else {
$viewport.html("<div style='color:#c0392b; padding:10px; font-size:11px;'>Invalid video ID sequence extracted.</div>");
}
} catch (err) {
console.error("Shorts engine exception:", err);
$viewport.text("Error handling text structure values.");
}
}).fail(function() {
$viewport.text("API connection failed.");
});
}
});
// Kasya
/* Open Timeline links in a new tab ONLY on the Chapters page */
mw.hook('wikipage.content').add(function($content) {
// Check if we are exactly on the timeline page
if (mw.config.get('wgPageName') === "Chapters_in_Srila_Prabhupada's_Life") {
// If yes, apply the new tab rule to the table
$content.find('#mp-tfp a').attr('target', '_blank').attr('rel', 'noopener noreferrer');
}
});
// Wait for MediaWiki core modules and the DOM to load
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
$(document).ready(function () {
// 1. FIND THE "102 Languages" SIDEBAR SECTION
var $sidebarMenu = $('#p-102_Languages ul').first();
// Fallback in case your specific MediaWiki version formats the ID differently
if ($sidebarMenu.length === 0) {
$('.vector-menu-heading, .portal h3').each(function() {
// Looks for the heading text directly
if ($(this).text().trim() === '102 Languages') {
$sidebarMenu = $(this).closest('.vector-menu, .portal').find('ul').first();
}
});
}
// Exit if the 102 Languages menu isn't found on the page
if (!$sidebarMenu || $sidebarMenu.length === 0) return;
// 2. CREATE THE SEARCH BAR AS A MENU ITEM
var $searchLi = $('<li id="n-language-search" style="margin-top: 10px; margin-bottom: 5px; list-style: none;"></li>');
var $searchBox = $('<input type="text" id="sidebarLanguageSearch" placeholder="Search 102 Languages..." style="width: 100%; box-sizing: border-box; padding: 5px; font-size: 0.85em; border: 1px solid #a2a9b1; border-radius: 2px;" autocomplete="off" />');
// Container for the results (hidden by default)
var $resultsContainer = $('<ul id="sidebarLanguageResults" style="list-style: none; padding: 0; margin: 4px 0 0 0; max-height: 200px; overflow-y: auto; border: 1px solid #eaecf0; background: #ffffff; display: none;"></ul>');
// Build the menu item and append it to the sidebar
$searchLi.append($searchBox).append($resultsContainer);
$sidebarMenu.prepend($searchLi);
var api = new mw.Api();
var languageList = [];
// 3. FETCH LANGUAGES FROM THE CATEGORY VIA API IN THE BACKGROUND
api.get({
action: 'query',
list: 'categorymembers',
cmtitle: 'Category:Languages_with_Total_Pages',
cmlimit: 'max',
format: 'json'
}).done(function (data) {
if (data.query && data.query.categorymembers) {
languageList = data.query.categorymembers.map(function (member) {
return {
title: member.title,
url: mw.util.getUrl(member.title)
};
});
}
});
// 4. ADD SEARCH FUNCTIONALITY TO THE SIDEBAR BOX
$('#sidebarLanguageSearch').on('keyup', function (e) {
e.stopPropagation();
var searchTerm = $(this).val().toLowerCase();
$resultsContainer.empty();
// Hide the results box if the search is empty
if (searchTerm.length === 0) {
$resultsContainer.hide();
return;
}
// Filter the fetched languages
var filteredLanguages = languageList.filter(function (lang) {
return lang.title.toLowerCase().indexOf(searchTerm) > -1;
});
// Show results container
$resultsContainer.show();
// Append results using Vector's standard dark text color
if (filteredLanguages.length > 0) {
filteredLanguages.forEach(function (lang) {
$resultsContainer.append('<li style="padding: 4px 6px; border-bottom: 1px solid #f8f9fa;"><a href="' + lang.url + '" style="text-decoration: none; color: #0645ad; display: block; font-size: 0.9em;">' + lang.title + '</a></li>');
});
} else {
$resultsContainer.append('<li style="padding: 4px 6px; color: #72777d; font-size: 0.9em;">No languages found</li>');
}
});
// Prevent typing from accidentally triggering global keyboard shortcuts
$('#sidebarLanguageSearch').on('keydown', function(e) {
e.stopPropagation();
});
// Optional: Hide results when clicking outside the sidebar search
$(document).on('click', function(e) {
if (!$(e.target).closest('#n-language-search').length) {
$resultsContainer.hide();
}
});
// Re-show results if they click back into the search box
$('#sidebarLanguageSearch').on('click', function() {
if ($(this).val().length > 0) {
$resultsContainer.show();
}
});
});
});
// 5. SIDEBAR ACCORDION - STRICT LEFT-COLUMN ISOLATION & TRUE ACCORDION
(function() {
// 1. Tag ONLY menus physically located in the left sidebar
function tagSidebarMenus() {
var allMenus = document.querySelectorAll('.vector-menu');
for (var i = 0; i < allMenus.length; i++) {
var menu = allMenus[i];
// STRICT INCLUSION: Only tag menus inside the modern or legacy left sidebar containers
if (menu.closest('#vector-main-menu, #mw-panel, #mw-navigation')) {
menu.classList.add('vanipedia-sidebar-menu');
// Collapse them by default if they have a heading
if (menu.querySelector('.vector-menu-heading') && !menu.classList.contains('vanipedia-collapsed')) {
menu.classList.add('vanipedia-collapsed');
}
}
}
}
// Run tagging immediately, and again on load as a failsafe
tagSidebarMenus();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tagSidebarMenus);
}
// 2. Inject CSS targeted STRICTLY at our newly tagged custom class
var style = document.createElement('style');
style.textContent =
/* Widen only our tagged sidebar menus */
'.vanipedia-sidebar-menu { width: 185px !important; } ' +
/* Core functional hiding rule */
'.vanipedia-sidebar-menu.vanipedia-collapsed .vector-menu-content { display: none !important; } ' +
/* Bold, Dark Header */
'.vanipedia-sidebar-menu .vector-menu-heading { ' +
' cursor: pointer !important; ' +
' background-color: #2c4c50 !important; ' +
' border: 1px solid #1b3033 !important; ' +
' border-radius: 4px !important; ' +
' margin-bottom: 4px !important; ' +
' padding: 10px 12px !important; ' +
' font-weight: 600 !important; ' +
' color: #ffffff !important; ' +
' display: flex !important; ' +
' justify-content: space-between !important; ' +
' align-items: center !important; ' +
' gap: 10px !important; ' +
' transition: all 0.2s ease; ' +
'} ' +
/* Fix header corners when expanded */
'.vanipedia-sidebar-menu:not(.vanipedia-collapsed) .vector-menu-heading { ' +
' border-radius: 4px 4px 0 0 !important; ' +
' margin-bottom: 0 !important; ' +
'} ' +
/* Hover effect */
'.vanipedia-sidebar-menu .vector-menu-heading:hover { background-color: #385e63 !important; } ' +
/* The dropdown caret */
'.vanipedia-sidebar-menu .vector-menu-heading::after { ' +
' content: "▼"; ' +
' font-size: 0.75em; ' +
' color: #ffffff !important; ' +
' transition: transform 0.2s ease; ' +
'} ' +
/* Rotate the arrow when collapsed */
'.vanipedia-sidebar-menu.vanipedia-collapsed .vector-menu-heading::after { ' +
' transform: rotate(-90deg); ' +
'} ' +
/* The Expanded Content Area */
'.vanipedia-sidebar-menu .vector-menu-content { ' +
' background-color: #eaeff0 !important; ' +
' border: 1px solid #2c4c50 !important; ' +
' border-top: none !important; ' +
' border-radius: 0 0 4px 4px !important; ' +
' padding: 0 0 4px 0 !important; ' +
' margin-bottom: 6px !important; ' +
' box-shadow: 0 3px 6px rgba(0,0,0,0.15) !important; ' +
'} ' +
/* Reset the list styles */
'.vanipedia-sidebar-menu .vector-menu-content ul { ' +
' list-style: none !important; ' +
' margin: 0 !important; ' +
' padding: 0 !important; ' +
'} ' +
/* Menu links */
'.vanipedia-sidebar-menu .vector-menu-content li a { ' +
' display: block !important; ' +
' padding: 8px 12px 8px 16px !important; ' +
' color: #1b3033 !important; ' +
' text-decoration: none !important; ' +
' border-left: 3px solid transparent !important; ' +
' border-bottom: 1px solid rgba(44, 76, 80, 0.1) !important; ' +
' transition: all 0.15s ease !important; ' +
'} ' +
/* Remove separator from last item */
'.vanipedia-sidebar-menu .vector-menu-content li:last-child a { ' +
' border-bottom: none !important; ' +
'} ' +
/* Hover effect for individual menu items */
'.vanipedia-sidebar-menu .vector-menu-content li a:hover { ' +
' background-color: #dbe4e6 !important; ' +
' border-left: 3px solid #2c4c50 !important; ' +
' color: #000000 !important; ' +
'} ';
document.head.appendChild(style);
// 3. Helper function to collapse all of our securely tagged menus
function collapseAllSidebarMenus() {
var menus = document.querySelectorAll('.vanipedia-sidebar-menu');
for (var i = 0; i < menus.length; i++) {
menus[i].classList.add('vanipedia-collapsed');
}
}
// 4. Intercept clicks for True Accordion Behavior
document.addEventListener('click', function(e) {
var heading = e.target.closest('.vector-menu-heading');
if (!heading) return;
// ONLY react if this click belongs to one of our tagged sidebar menus
var navContainer = heading.closest('.vanipedia-sidebar-menu');
if (navContainer) {
var isAlreadyOpen = !navContainer.classList.contains('vanipedia-collapsed');
// Forcefully collapse ALL left sidebar menus first
collapseAllSidebarMenus();
// Open the clicked one if it wasn't already open
if (!isAlreadyOpen) {
navContainer.classList.remove('vanipedia-collapsed');
}
e.stopPropagation();
e.preventDefault();
}
}, true);
})();
mw.loader.using('mediawiki.user').then(function() {
// Fetch the user's assigned groups
var userGroups = mw.config.get('wgUserGroups') || [];
// Define the roles that SHOULD see the administrative tabs
var isAuthorized = userGroups.includes('sysop') ||
userGroups.includes('interface-admin') ||
userGroups.includes('Editor');
// If the user is not in one of those groups, restrict the UI
if (!isAuthorized) {
document.body.classList.add('restricted-ui');
}
});
// Function to open accordion from TOC and jump there //
mw.hook('wikipage.content').add(function() {
function handleAccordionTOC(hash) {
if (!hash || hash === '#') return false;
try {
var targetId = hash.substring(1);
var targetElement = document.getElementById(targetId) || document.getElementById(decodeURIComponent(targetId));
if (!targetElement) return false;
var $target = $(targetElement);
// Match either h2 or h3
var $heading = $target.is('h2, h3') ? $target : $target.closest('h2, h3');
if (!$heading.length) return false;
var $accordion = $heading.next('.book-accordion');
// If the heading itself is not followed directly by an accordion,
// check if the heading is located INSIDE a parent accordion (e.g., nested h3)
var $parentAccordions = $heading.parents('.book-accordion');
// If there's no direct accordion next to it and it's not inside any accordion,
// let the browser handle standard jump.
if (!$accordion.length && !$parentAccordions.length) return false;
// 1. Open all ancestor accordions (if target heading is nested inside one)
$parentAccordions.each(function() {
var $parentCollapsible = $(this).children('.mw-collapsible');
if ($parentCollapsible.length && $parentCollapsible.hasClass('mw-collapsed')) {
var pId = $parentCollapsible.attr('id');
if (pId) {
var pToggleClass = pId.replace('mw-customcollapsible-', 'mw-customtoggle-');
$('.' + pToggleClass).first().click();
}
}
});
// 2. Open the accordion immediately following this heading (if present)
if ($accordion.length) {
var $collapsible = $accordion.children('.mw-collapsible, .mw-collapsible-content').first();
if (!$collapsible.length) {
$collapsible = $accordion.find('.mw-collapsible');
}
if ($collapsible.length && $collapsible.hasClass('mw-collapsed')) {
var collapsibleId = $collapsible.attr('id');
if (collapsibleId) {
var toggleClass = collapsibleId.replace('mw-customcollapsible-', 'mw-customtoggle-');
$('.' + toggleClass).first().click();
}
}
}
// Target element to scroll to (the accordion, or the heading itself if accordion is missing)
var $scrollTarget = $accordion.length ? $accordion : $heading;
// 3. Scroll to the visible section
setTimeout(function() {
$('html, body').animate({
scrollTop: $scrollTarget.offset().top - 75
}, 300);
}, 100);
// Return true to indicate we successfully handled the TOC accordion click
return true;
} catch (e) {
console.error("TOC Accordion script error:", e);
return false;
}
}
$(document).off('click.tocAccordion').on('click.tocAccordion', 'a[href^="#"]', function(e) {
if ($(this).closest('#toc, .toc, .vector-toc').length > 0) {
var href = $(this).attr('href');
// Run our function to see if this is an accordion/heading link
var isAccordion = handleAccordionTOC(href);
// ONLY stop the default browser jump if we handled it
if (isAccordion) {
e.preventDefault();
// Update the URL manually so the user can still share the link
if (history.pushState) {
history.pushState(null, null, href);
} else {
window.location.hash = href;
}
}
}
});
if (window.location.hash) {
setTimeout(function() {
handleAccordionTOC(window.location.hash);
}, 500);
}
});
/* Sticky burger menu - Pure Floating Dropdown (Forces Unpinned & Full Title) */
(function () {
let isMenuOpen = false;
// 1. Force the sidebar out of pinned mode and return menu into the dropdown container
function forceUnpinSidebar() {
document.documentElement.classList.remove('vector-feature-main-menu-pinned-enabled', 'vector-feature-main-menu-pinned-clientpref-1');
document.documentElement.classList.add('vector-feature-main-menu-pinned-disabled', 'vector-feature-main-menu-pinned-clientpref-0');
if (document.body) {
document.body.classList.remove('vector-feature-main-menu-pinned-enabled', 'vector-feature-main-menu-pinned-clientpref-1');
document.body.classList.add('vector-feature-main-menu-pinned-disabled', 'vector-feature-main-menu-pinned-clientpref-0');
}
// Clear stored pin preference
document.cookie = "vector-feature-main-menu-pinned=0; path=/; max-age=31536000";
try { localStorage.setItem('vector-feature-main-menu-pinned', '0'); } catch(e) {}
// If #vector-main-menu was moved into the pinned container, move it back into dropdown
const mainMenu = document.getElementById('vector-main-menu');
const unpinnedContainer = document.getElementById('vector-main-menu-unpinned-container') ||
document.querySelector('#vector-main-menu-dropdown .vector-dropdown-content');
const pinnedContainer = document.getElementById('vector-main-menu-pinned-container');
if (mainMenu && unpinnedContainer && pinnedContainer && pinnedContainer.contains(mainMenu)) {
unpinnedContainer.appendChild(mainMenu);
}
}
function getDropdownElement() {
return document.querySelector('#vector-main-menu-dropdown .vector-dropdown-content') ||
document.getElementById('vector-main-menu-dropdown-content') ||
document.getElementById('vector-main-menu-unpinned-container') ||
document.getElementById('vector-main-menu');
}
function openMenu(menuBtn) {
forceUnpinSidebar();
const checkbox = document.getElementById('vector-main-menu-dropdown-checkbox');
const dropdown = getDropdownElement();
if (!dropdown) return;
if (checkbox) {
checkbox.checked = true;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
}
const rect = menuBtn.getBoundingClientRect();
dropdown.style.setProperty('position', 'fixed', 'important');
dropdown.style.setProperty('top', rect.bottom + 'px', 'important');
dropdown.style.setProperty('left', rect.left + 'px', 'important');
dropdown.style.setProperty('z-index', '99999', 'important');
dropdown.style.setProperty('max-height', (window.innerHeight - rect.bottom - 16) + 'px', 'important');
dropdown.style.setProperty('overflow-y', 'auto', 'important');
dropdown.style.setProperty('display', 'block', 'important');
dropdown.style.setProperty('visibility', 'visible', 'important');
dropdown.style.setProperty('opacity', '1', 'important');
dropdown.style.setProperty('background-color', 'var(--background-color-base, #ffffff)', 'important');
dropdown.style.setProperty('box-shadow', '0 4px 16px rgba(0, 0, 0, 0.2)', 'important');
dropdown.style.setProperty('border-radius', '4px', 'important');
isMenuOpen = true;
}
function closeMenu() {
const checkbox = document.getElementById('vector-main-menu-dropdown-checkbox');
const dropdown = getDropdownElement();
if (!dropdown) return;
if (checkbox) {
checkbox.checked = false;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
}
dropdown.style.removeProperty('position');
dropdown.style.removeProperty('top');
dropdown.style.removeProperty('left');
dropdown.style.removeProperty('z-index');
dropdown.style.removeProperty('max-height');
dropdown.style.removeProperty('overflow-y');
dropdown.style.removeProperty('background-color');
dropdown.style.removeProperty('box-shadow');
dropdown.style.removeProperty('border-radius');
dropdown.style.setProperty('display', 'none', 'important');
dropdown.style.setProperty('visibility', 'hidden', 'important');
dropdown.style.setProperty('opacity', '0', 'important');
isMenuOpen = false;
}
function initStickyBurger() {
forceUnpinSidebar();
const stickyHeader = document.getElementById('vector-sticky-header');
if (!stickyHeader) return false;
if (document.getElementById('vanipedia-sticky-main-menu')) return true;
// 1. Create burger button
const menuBtn = document.createElement('div');
menuBtn.id = 'vanipedia-sticky-main-menu';
menuBtn.title = 'Main menu';
menuBtn.style.cursor = 'pointer';
menuBtn.className = 'vector-dropdown-label cdx-button cdx-button--fake-button cdx-button--icon-only';
// 2. Add hamburger icon
const iconSpan = document.createElement('span');
iconSpan.className = 'vector-icon mw-ui-icon-menu mw-ui-icon-wikimedia-menu';
menuBtn.appendChild(iconSpan);
// 3. Toggle click handler (1st click opens, 2nd click rolls back)
menuBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
if (isMenuOpen) {
closeMenu();
} else {
openMenu(menuBtn);
}
});
// 4. Inject into start container
let startContainer = stickyHeader.querySelector('.vector-sticky-header-start');
if (!startContainer) {
startContainer = document.createElement('div');
startContainer.className = 'vector-sticky-header-start';
stickyHeader.insertBefore(startContainer, stickyHeader.firstChild);
}
startContainer.insertBefore(menuBtn, startContainer.firstChild);
return true;
}
// 5. Close when clicking anywhere outside
document.addEventListener('click', function (e) {
if (!isMenuOpen) return;
const menuBtn = document.getElementById('vanipedia-sticky-main-menu');
const dropdown = getDropdownElement();
if (dropdown && menuBtn && !menuBtn.contains(e.target) && !dropdown.contains(e.target)) {
closeMenu();
}
});
// 6. Bootstrap initializer
if (!initStickyBurger()) {
const checkTimer = setInterval(function () {
if (initStickyBurger()) {
clearInterval(checkTimer);
}
}, 100);
setTimeout(function () { clearInterval(checkTimer); }, 4000);
}
mw.hook('wikipage.content').add(initStickyBurger);
$(initStickyBurger);
})();
/* Floating "Back to Top" Button Controller */
(function () {
function initBackToTop() {
if (document.getElementById('vanipedia-back-to-top')) return;
// 1. Create button element with an up-arrow SVG icon
const btn = document.createElement('button');
btn.id = 'vanipedia-back-to-top';
btn.title = 'Back to top';
btn.setAttribute('aria-label', 'Back to top');
btn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
`;
// 2. Smooth scroll to top on click
btn.addEventListener('click', function (e) {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// 3. Show button only after scrolling down 300px
function toggleVisibility() {
if (window.scrollY > 300) {
btn.classList.add('visible');
} else {
btn.classList.remove('visible');
}
}
window.addEventListener('scroll', toggleVisibility, { passive: true });
toggleVisibility();
document.body.appendChild(btn);
}
$(initBackToTop);
mw.hook('wikipage.content').add(initBackToTop);
})();
(function () {
if (!document.getElementById('vani-home')) {
return;
}
/* =====================================================
ROTATING HERO PHOTOGRAPHS
===================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const vaniHeroImages = [
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-01.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-02.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-03.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-04.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-05.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-06.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-07.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-08.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-09.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-10.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-11.jpg',
'/wiki/Special:Redirect/file/Srila-Prabhupada-Hero-12.jpg'
];
const heroA = document.querySelector('.vh-photo-a');
const heroB = document.querySelector('.vh-photo-b');
if (heroA && heroB && vaniHeroImages.length) {
let currentImage = -1;
let activeLayer = heroA;
function chooseHeroImage() {
let next;
do {
next = Math.floor(
Math.random() * vaniHeroImages.length
);
} while (
next === currentImage &&
vaniHeroImages.length > 1
);
return next;
}
function changeHero(firstLoad) {
const next = chooseHeroImage();
const image = vaniHeroImages[next];
if (firstLoad) {
heroA.style.backgroundImage =
'url("' + image + '")';
heroA.style.opacity = '1';
heroB.style.opacity = '0';
activeLayer = heroA;
currentImage = next;
return;
}
const incoming =
activeLayer === heroA ? heroB : heroA;
incoming.style.backgroundImage =
'url("' + image + '")';
incoming.style.opacity = '1';
activeLayer.style.opacity = '0';
activeLayer = incoming;
currentImage = next;
}
changeHero(true);
window.setInterval(function () {
changeHero(false);
}, 10000);
}
}
/* =====================================================
ROTATING ŚRĪLA PRABHUPĀDA SUBTITLE
===================================================== */
const vaniSubtitles = [
'His words still speak to us today.',
'His vāṇī brings closer to Kṛṣṇa.<br>His vision gives direction in lives.',
'Words spoken decades ago.<br>Wisdom that speaks personally today.',
'His vāṇī lives when we hear it and share with others.',
'There is always more to discover.<br>More to hear. More to understand. More to live.',
'His teachings give direction.<br>His example gives courage to follow.',
'From his heart —<br>a treasure of teachings for every generation.',
'His vāṇī. His compassion. His example.',
'His vāṇī lives on. His teachings guide us.<br>His mission continues.',
'His words cross decades and continents —<br>still reaching hearts, still changing lives.',
'His words continue to speak.<br>His teachings continue to guide.',
'He gave us a great treasure.<br>Vanipedia invites you to enter it.',
'A lifetime of teachings.<br>Invitation to hear him with an open heart.',
'His words illuminate the path.<br>His example inspires us to walk it.',
'He carried Kṛṣṇa consciousness across the world.<br>His vāṇī continues that journey today.',
'He lives on through his words,<br>his teachings and the mission he gave us.',
'His words continue to speak.<br>His teachings continue to guide.',
'Preserving and sharing Śrīla Prabhupāda’s vāṇī<br>so that generations may continue to hear from him.'
];
const vaniSubtitle =
document.getElementById('vh-subtitle-rotation');
if (vaniSubtitle) {
let currentSubtitle = -1;
function chooseSubtitle() {
let next;
do {
next = Math.floor(
Math.random() * vaniSubtitles.length
);
} while (
next === currentSubtitle &&
vaniSubtitles.length > 1
);
return next;
}
function showSubtitle(firstLoad) {
const next = chooseSubtitle();
if (firstLoad) {
currentSubtitle = next;
vaniSubtitle.innerHTML = vaniSubtitles[next];
vaniSubtitle.classList.remove('is-changing');
return;
}
// Fade out
vaniSubtitle.classList.add('is-changing');
window.setTimeout(function () {
vaniSubtitle.innerHTML = vaniSubtitles[next];
currentSubtitle = next;
requestAnimationFrame(function () {
requestAnimationFrame(function () {
vaniSubtitle.classList.remove('is-changing');
});
});
}, 450);
}
/* Initial sentence */
showSubtitle(true);
/* Change every 10 seconds */
window.setInterval(function () {
showSubtitle(false);
}, 12000);
}
/* ==========================================================
MAIN PAGE — REMOVE NATIVE LINK TOOLTIPS
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
function removeMainPageLinkTitles() {
document
.querySelectorAll('#vani-home a[title]')
.forEach(function (link) {
link.removeAttribute('title');
});
}
$(removeMainPageLinkTitles);
mw.hook('wikipage.content').add(function () {
removeMainPageLinkTitles();
});
}
/* =====================================================
RANDOM THEMATIC ARTICLE SPOTLIGHT
===================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const discoverBox = document.getElementById('vh-random-article');
if (discoverBox) {
let articleLinks = [];
let currentHref = null;
const sourceUrl =
mw.util.getUrl('Category:Vanipedia Gemini - Articles');
function loadArticleLinks() {
return fetch(sourceUrl)
.then(function (response) {
return response.text();
})
.then(function (html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const links = Array.from(
doc.querySelectorAll('#mw-content-text a')
);
articleLinks = links
.map(function (a) {
return {
title: a.textContent.trim(),
href: a.getAttribute('href')
};
})
.filter(function (item) {
if (!item.title || !item.href) {
return false;
}
if (!item.href.startsWith('/wiki/')) {
return false;
}
if (item.href.includes('Category:')) {
return false;
}
if (item.href.includes('Special:')) {
return false;
}
return true;
});
console.log(
'Spotlight links loaded:',
articleLinks.length
);
});
}
function chooseRandomArticle() {
if (!articleLinks.length) {
return null;
}
let chosen;
do {
chosen =
articleLinks[
Math.floor(Math.random() * articleLinks.length)
];
} while (
chosen.href === currentHref &&
articleLinks.length > 1
);
return chosen;
}
function getShortTitle(youtubeUrl) {
const url =
'https://www.youtube.com/oembed' +
'?url=' +
encodeURIComponent(youtubeUrl) +
'&format=json';
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'YouTube title HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
return (
data &&
data.title
? data.title.trim()
: ''
);
})
.catch(function(error) {
console.warn(
'Could not load Short title:',
error
);
return '';
});
}
function showRandomArticle() {
const article = chooseRandomArticle();
if (!article) {
return;
}
currentHref = article.href;
const title =
decodeURIComponent(
article.href
.replace(/^\/wiki\//, '')
.replace(/_/g, ' ')
);
const apiUrl =
mw.util.wikiScript('api') +
'?action=query' +
'&format=json' +
'&prop=extracts' +
'&exintro=1' +
'&explaintext=1' +
'&exchars=260' +
'&titles=' +
encodeURIComponent(title);
fetch(apiUrl)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (!data.query || !data.query.pages) {
return;
}
const page = Object.values(data.query.pages)[0];
/* Do not display pages without a real extract */
if (
!page ||
!page.extract ||
page.extract.trim().length < 40
) {
showRandomArticle();
return;
}
const extract = page.extract.trim();
/* Fetch is finished. NOW begin the fade. */
discoverBox.classList.add('is-changing');
window.setTimeout(function () {
discoverBox.innerHTML =
'<a class="vh-random-article-link" href="' +
article.href +
'">' +
'<div class="vh-random-article-title">' +
escapeHtml(article.title) +
'</div>' +
'<div class="vh-random-article-extract">' +
escapeHtml(extract) +
'</div>' +
'<div class="vh-random-article-more">' +
'Read article →' +
'</div>' +
'</a>';
requestAnimationFrame(function () {
discoverBox.classList.remove('is-changing');
});
}, 500);
})
.catch(function () {
/* Simply try another article */
showRandomArticle();
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
loadArticleLinks()
.then(function () {
if (!articleLinks.length) {
return;
}
showRandomArticle();
window.setInterval(function () {
showRandomArticle();
}, 11000);
})
.catch(function (error) {
console.error(
'Spotlight failed:',
error
);
});
}
}
/* =====================================================
FROM HIS LETTERS
Random Śrīla Prabhupāda letter from Vanisource
===================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const letterBox =
document.getElementById('vh-random-letter');
if (letterBox) {
const vanisourceApi =
'https://vanisource.org/w/api.php';
const rootCategory =
'Category:Letters from Srila Prabhupada - by person';
let recipientCategories = [];
let currentLetter = '';
let letterChanging = false;
/* -------------------------------------------------
LOAD RECIPIENT CATEGORIES
------------------------------------------------- */
function loadLetterRecipients(cmcontinue) {
let url =
vanisourceApi +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(rootCategory) +
'&cmtype=subcat' +
'&cmlimit=500';
if (cmcontinue) {
url +=
'&cmcontinue=' +
encodeURIComponent(cmcontinue);
}
return fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (
data.query &&
data.query.categorymembers
) {
data.query.categorymembers
.forEach(function (item) {
if (
item.title &&
item.title.endsWith(
' - Letters'
)
) {
recipientCategories.push(
item.title
);
}
});
}
/* Continue if more than 500 */
if (
data.continue &&
data.continue.cmcontinue
) {
return loadLetterRecipients(
data.continue.cmcontinue
);
}
});
}
/* -------------------------------------------------
CHOOSE RANDOM RECIPIENT
------------------------------------------------- */
function chooseRecipient() {
if (!recipientCategories.length) {
return null;
}
return recipientCategories[
Math.floor(
Math.random() *
recipientCategories.length
)
];
}
/* -------------------------------------------------
GET LETTERS FOR THAT RECIPIENT
------------------------------------------------- */
function getRandomLetter(category) {
const url =
vanisourceApi +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(category) +
'&cmnamespace=0' +
'&cmtype=page' +
'&cmlimit=500';
return fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (
!data.query ||
!data.query.categorymembers ||
!data.query.categorymembers.length
) {
return null;
}
const letters =
data.query.categorymembers;
let letter;
do {
letter =
letters[
Math.floor(
Math.random() *
letters.length
)
];
} while (
currentLetter &&
letter.title === currentLetter &&
letters.length > 1
);
return letter;
});
}
/* -------------------------------------------------
RECIPIENT NAME
------------------------------------------------- */
function getRecipientName(category) {
return category
.replace(/^Category:/, '')
.replace(/ - Letters$/, '')
.trim();
}
/* -------------------------------------------------
TRY TO EXTRACT DATE FROM LETTER PAGE TITLE
Supports dates at beginning such as:
690101
740516
etc.
------------------------------------------------- */
function getLetterDate(title) {
const match =
title.match(
/(?:^|\/)(\d{2})(\d{2})(\d{2})(?:\D|$)/
);
if (!match) {
return '';
}
const yy =
parseInt(match[1], 10);
const month =
parseInt(match[2], 10);
const day =
parseInt(match[3], 10);
if (
month < 1 ||
month > 12 ||
day < 1 ||
day > 31
) {
return '';
}
const year =
1900 + yy;
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return (
day +
' ' +
months[month - 1] +
' ' +
year
);
}
/* -------------------------------------------------
GET ACTUAL LETTER CONTENT
------------------------------------------------- */
function getLetterPassage(letterTitle) {
const url =
vanisourceApi +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(letterTitle) +
'&prop=text';
return fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (
!data.parse ||
!data.parse.text ||
!data.parse.text['*']
) {
return '';
}
const wrapper =
document.createElement('div');
wrapper.innerHTML =
data.parse.text['*'];
/*
* Remove things which definitely are
* not part of the actual letter.
*/
wrapper.querySelectorAll(
'table, .navbox, .toc, .mw-editsection, ' +
'.metadata, .noprint, style, script'
).forEach(function (element) {
element.remove();
});
/*
* Collect useful paragraphs.
*/
let paragraphs =
Array.from(
wrapper.querySelectorAll('p')
)
.map(function (p) {
return p.textContent
.replace(/\s+/g, ' ')
.trim();
})
.filter(function (text) {
if (text.length < 120) {
return false;
}
const lower = text.toLowerCase();
/* Standard opening/closing formalities */
if (
lower.startsWith('dear ') ||
lower.startsWith('my dear ') ||
lower.includes('please accept my blessings') ||
lower.includes('please accept my humble') ||
lower.includes('i beg to acknowledge receipt') ||
lower.includes('i am in due receipt') ||
lower.includes('your ever well-wisher') ||
lower.includes('hoping this will meet you') ||
lower.includes('yours in the service') ||
lower.includes('your servant')
) {
return false;
}
/* Addresses / letter headers */
if (
lower.includes('iskcon') ||
lower.includes('international society for krishna consciousness') ||
lower.includes('founder-acarya') ||
lower.includes('founder acharya') ||
lower.includes('a.c. bhaktivedanta swami') ||
lower.includes('tridandi goswami') ||
lower.includes('tridandi gosvami') ||
lower.includes('bhaktivedanta manor') ||
lower.includes('hare krishna land')
) {
return false;
}
/* Looks too much like an address */
const addressWords = [
'road',
'street',
'avenue',
'building',
'apartment',
'bombay',
'calcutta',
'los angeles',
'new york',
'london'
];
let addressHits = 0;
addressWords.forEach(function (word) {
if (lower.includes(word)) {
addressHits++;
}
});
if (addressHits >= 2) {
return false;
}
/*
* Require something resembling actual prose:
* at least two sentence endings.
*/
const sentences =
(text.match(/[.!?](?:\s|$)/g) || []).length;
if (sentences < 2) {
return false;
}
return true;
});
if (!paragraphs.length) {
return '';
}
/*
* Prefer reasonably substantial passages.
*/
let preferred =
paragraphs.filter(function (text) {
return (
text.length >= 180 &&
text.length <= 400
);
});
if (!preferred.length) {
preferred = paragraphs;
}
/*
* Random meaningful paragraph from
* within the letter.
*/
let passage =
preferred[
Math.floor(
Math.random() *
preferred.length
)
];
/*
* Prevent enormous passage in narrow
* left column.
*/
if (passage.length > 300) {
passage =
passage.substring(0, 300);
const lastSpace =
passage.lastIndexOf(' ');
if (lastSpace > 250) {
passage =
passage.substring(
0,
lastSpace
);
}
passage += '…';
}
return passage;
});
}
/* -------------------------------------------------
SAFE HTML
------------------------------------------------- */
function escapeLetterHtml(text) {
const div =
document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/* -------------------------------------------------
BUILD VANISOURCE URL
------------------------------------------------- */
function getLetterUrl(title) {
return (
'https://vanisource.org/wiki/' +
encodeURIComponent(
title.replace(/ /g, '_')
)
.replace(/%2F/g, '/')
.replace(/%3A/g, ':')
);
}
/* -------------------------------------------------
FIND A VALID RANDOM LETTER
------------------------------------------------- */
function prepareRandomLetter(attempt) {
attempt = attempt || 0;
/*
* Prevent an endless retry loop if Vanisource
* unexpectedly returns unusable pages.
*/
if (attempt > 15) {
return Promise.resolve(null);
}
const category =
chooseRecipient();
if (!category) {
return Promise.resolve(null);
}
return getRandomLetter(category)
.then(function (letter) {
if (!letter || !letter.title) {
return prepareRandomLetter(
attempt + 1
);
}
return getLetterPassage(
letter.title
)
.then(function (passage) {
if (
!passage ||
passage.length < 80
) {
return prepareRandomLetter(
attempt + 1
);
}
return {
title:
letter.title,
recipient:
getRecipientName(
category
),
date:
getLetterDate(
letter.title
),
passage:
passage,
href:
getLetterUrl(
letter.title
)
};
});
});
}
/* -------------------------------------------------
DISPLAY LETTER
------------------------------------------------- */
function showRandomLetter() {
if (letterChanging) {
return;
}
letterChanging = true;
/*
* Prepare the next letter while the current
* one remains fully visible.
*/
prepareRandomLetter(0)
.then(function (letter) {
if (!letter) {
letterChanging = false;
return;
}
currentLetter =
letter.title;
/*
* Only start fading AFTER the new
* content is ready.
*/
letterBox.classList.add(
'is-changing'
);
window.setTimeout(
function () {
let meta = '';
if (letter.date) {
meta =
'<div class="vh-random-letter-date">' +
escapeLetterHtml(
letter.date
) +
'</div>';
}
letterBox.innerHTML =
'<a class="vh-random-letter-link" ' +
'href="' +
letter.href +
'">' +
'<div class="vh-random-letter-person">' +
'To ' +
escapeLetterHtml(
letter.recipient
) +
'</div>' +
meta +
'<div class="vh-random-letter-text">' +
escapeLetterHtml(
letter.passage
) +
'</div>' +
'<div class="vh-random-letter-more">' +
'Read letter →' +
'</div>' +
'</a>';
requestAnimationFrame(
function () {
letterBox.classList.remove(
'is-changing'
);
letterChanging = false;
}
);
},
450
);
})
.catch(function (error) {
console.error(
'Letter feature failed:',
error
);
letterChanging = false;
});
}
/* -------------------------------------------------
START
------------------------------------------------- */
loadLetterRecipients()
.then(function () {
if (!recipientCategories.length) {
console.warn(
'No Vanisource letter recipients found.'
);
return;
}
console.log(
'Letter recipient categories loaded:',
recipientCategories.length
);
/*
* First letter immediately.
*/
showRandomLetter();
/*
* New letter every 10 seconds.
*/
window.setInterval(
showRandomLetter,
10000
);
})
.catch(function (error) {
console.error(
'Could not load Vanisource letters:',
error
);
});
}
}
/* ==========================================================
VIRTUAL CONTINUOUS ŚRĪLA PRABHUPĀDA TIMELINE
Includes 1896–1964 + 1965–1977
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const timelineWindow =
document.querySelector('.vh-lifeline-window');
const timelineTrack =
document.getElementById('vh-timeline-track');
const yearButtons =
document.querySelectorAll(
'#vh-timeline-years .vh-year'
);
if (timelineWindow && timelineTrack) {
const API =
'https://vanipedia.org/w/api.php';
const ITEM_WIDTH = 220;
const AUTO_SPEED = 52;
const VISIBLE_ITEMS = 22;
let events = [];
let nodes = [];
let firstIndex = 0;
let offsetX = 0;
let dragging = false;
let dragMoved = false;
let pointerStartX = 0;
let previousPointerX = 0;
let lastFrame =
performance.now();
let animationId = null;
/* ==================================================
TEXT HELPERS
================================================== */
function cleanText(text) {
return text
.replace(/\s+/g, ' ')
.replace(/^[\s\-–—:,]+/, '')
.trim();
}
function shortenText(text) {
text =
cleanText(text);
if (text.length <= 95) {
return text;
}
let result =
text.substring(0, 95);
const space =
result.lastIndexOf(' ');
if (space > 70) {
result =
result.substring(
0,
space
);
}
return result + '…';
}
/* ==================================================
DEFAULT PAGE LINK
================================================== */
function makePageUrl(page) {
return (
'https://vanipedia.org/wiki/' +
encodeURIComponent(
page.replace(/ /g, '_')
)
.replace(/%2F/g, '/')
.replace(/%3A/g, ':')
);
}
/* ==================================================
FIND BEST LINK INSIDE ENTRY
================================================== */
function getUsefulLink(
element,
fallback
) {
const usefulLink =
Array
.from(
element.querySelectorAll('a')
)
.find(
function (a) {
const h =
a.getAttribute(
'href'
);
return (
h &&
!h.includes(
'action=edit'
) &&
!h.includes(
'redlink=1'
)
);
}
);
if (!usefulLink) {
return fallback;
}
const rawHref =
usefulLink.getAttribute(
'href'
);
if (!rawHref) {
return fallback;
}
try {
return new URL(
rawHref,
'https://vanipedia.org'
).href;
} catch (e) {
return fallback;
}
}
/* ==================================================
FETCH 1896–1964
Main timeline page
================================================== */
function fetchEarlyTimeline() {
const page =
"Timeline of Srila Prabhupada's Life";
const url =
API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(page) +
'&prop=text';
return fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (
!data.parse ||
!data.parse.text ||
!data.parse.text['*']
) {
return [];
}
const temp =
document.createElement('div');
temp.innerHTML =
data.parse.text['*'];
const result = [];
temp
.querySelectorAll('li')
.forEach(function (li) {
const fullText =
cleanText(
li.textContent
);
/*
* Expected examples:
*
* 1896 - September 01 - Calcutta - ...
* 1922 - Meets ...
* 1944 - February 14 - Publishes ...
* 1959 - September 17 - ...
*
* We only keep 1896–1964.
*/
const match =
fullText.match(
/^((?:18|19)\d{2})\s*[-–—]\s*(.+)$/i
);
if (!match) {
return;
}
const year =
parseInt(
match[1],
10
);
if (
year < 1896 ||
year > 1964
) {
return;
}
let remainder =
cleanText(
match[2]
);
if (!remainder) {
return;
}
/*
* Try to extract a date
* from the remainder.
*/
let displayDate =
String(year);
let description =
remainder;
const monthMatch =
remainder.match(
/^(January|February|March|April|May|June|July|August|September|October|November|December)(?:\s+(\d{1,2}))?\s*[-–—]\s*(.+)$/i
);
if (monthMatch) {
const month =
monthMatch[1];
const day =
monthMatch[2]
? monthMatch[2]
: '';
description =
cleanText(
monthMatch[3]
);
displayDate =
month;
if (day) {
displayDate +=
' ' +
day;
}
displayDate +=
', ' +
year;
} else {
/*
* If no month is present,
* show year only.
*/
description =
remainder;
}
description =
shortenText(
description
);
if (!description) {
return;
}
const fallback =
makePageUrl(page);
const href =
getUsefulLink(
li,
fallback
);
result.push({
year:
year,
group:
'1896-1964',
date:
displayDate,
text:
description,
href:
href
});
});
return result;
});
}
/* ==================================================
FETCH ONE YEAR: 1965–1977
================================================== */
function fetchYear(year) {
const page =
"Timeline of Srila Prabhupada's Life - " +
year;
const url =
API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(page) +
'&prop=text';
return fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
if (
!data.parse ||
!data.parse.text ||
!data.parse.text['*']
) {
return [];
}
const temp =
document.createElement('div');
temp.innerHTML =
data.parse.text['*'];
const result = [];
temp
.querySelectorAll('li')
.forEach(function (li) {
const fullText =
cleanText(
li.textContent
);
const match =
fullText.match(
/^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})(?:,?\s+([0-9]{1,2}(?::[0-9]{2})?(?:\s*[AP]\.?M\.?)?))?\s*[-–—]\s*(.+)$/i
);
if (!match) {
return;
}
const month =
match[1];
const day =
match[2];
const time =
match[3]
? match[3].trim()
: '';
const description =
shortenText(
match[4]
);
if (!description) {
return;
}
const fallback =
makePageUrl(page);
const href =
getUsefulLink(
li,
fallback
);
let date =
month +
' ' +
day +
', ' +
year;
if (time) {
date +=
' · ' +
time;
}
result.push({
year:
year,
group:
String(year),
date:
date,
text:
description,
href:
href
});
});
return result;
});
}
/* ==================================================
CREATE NODE
================================================== */
function createNode() {
const link =
document.createElement('a');
link.className =
'vh-timeline-item';
link.draggable =
false;
const date =
document.createElement('div');
date.className =
'vh-timeline-date';
const dot =
document.createElement('span');
dot.className =
'vh-timeline-dot';
const text =
document.createElement('div');
text.className =
'vh-timeline-text';
link.appendChild(date);
link.appendChild(dot);
link.appendChild(text);
link.addEventListener(
'click',
function (event) {
if (dragMoved) {
event.preventDefault();
}
}
);
return link;
}
/* ==================================================
POPULATE NODE
================================================== */
function populateNode(
node,
eventIndex
) {
const total =
events.length;
eventIndex =
(
(
eventIndex %
total
) +
total
) %
total;
const item =
events[eventIndex];
node.href =
item.href;
node.dataset.year =
item.year;
node.dataset.group =
item.group;
node
.querySelector(
'.vh-timeline-date'
)
.textContent =
item.date;
node
.querySelector(
'.vh-timeline-text'
)
.textContent =
item.text;
}
/* ==================================================
BUILD VIRTUAL STRIP
================================================== */
function buildVirtualTimeline() {
timelineTrack.innerHTML = '';
nodes = [];
const needed =
Math.max(
VISIBLE_ITEMS,
Math.ceil(
timelineWindow.clientWidth /
ITEM_WIDTH
) + 6
);
for (
let i = 0;
i < needed;
i++
) {
const node =
createNode();
populateNode(
node,
firstIndex + i
);
timelineTrack.appendChild(
node
);
nodes.push(
node
);
}
timelineTrack.style.width =
(
needed *
ITEM_WIDTH
) +
'px';
offsetX =
-ITEM_WIDTH;
firstIndex =
(
firstIndex -
1 +
events.length
) %
events.length;
refreshAllNodes();
draw();
}
function refreshAllNodes() {
nodes.forEach(
function (
node,
position
) {
populateNode(
node,
firstIndex +
position
);
}
);
}
/* ==================================================
RECYCLE LEFT
================================================== */
function recycleLeft() {
const node =
nodes.shift();
nodes.push(
node
);
timelineTrack.appendChild(
node
);
firstIndex =
(
firstIndex +
1
) %
events.length;
populateNode(
node,
firstIndex +
nodes.length -
1
);
offsetX +=
ITEM_WIDTH;
}
/* ==================================================
RECYCLE RIGHT
================================================== */
function recycleRight() {
const node =
nodes.pop();
firstIndex =
(
firstIndex -
1 +
events.length
) %
events.length;
nodes.unshift(
node
);
timelineTrack.insertBefore(
node,
timelineTrack.firstChild
);
populateNode(
node,
firstIndex
);
offsetX -=
ITEM_WIDTH;
}
function normalizePosition() {
while (
offsetX <=
-ITEM_WIDTH * 2
) {
recycleLeft();
}
while (
offsetX > 0
) {
recycleRight();
}
}
/* ==================================================
DRAW
================================================== */
function draw() {
timelineTrack.style.transform =
'translate3d(' +
offsetX +
'px,0,0)';
}
/* ==================================================
ACTIVE YEAR BUTTON
================================================== */
function setActiveButton(group) {
yearButtons.forEach(
function (button) {
const value =
button.classList.contains(
'vh-year-early'
)
? '1896-1964'
: button.textContent.trim();
button.classList.toggle(
'is-active',
value === group
);
}
);
}
/* ==================================================
JUMP TO YEAR / PERIOD
================================================== */
function jumpToGroup(group) {
if (!events.length) {
return;
}
let targetIndex;
if (group === '1896-1964') {
targetIndex =
events.findIndex(
function (item) {
return (
item.group ===
'1896-1964'
);
}
);
} else {
const year =
parseInt(
group,
10
);
targetIndex =
events.findIndex(
function (item) {
return (
item.year ===
year
);
}
);
}
if (targetIndex === -1) {
return;
}
/*
* Put target near left side,
* with one virtual buffer item before it.
*/
firstIndex =
(
targetIndex -
1 +
events.length
) %
events.length;
offsetX =
-ITEM_WIDTH;
refreshAllNodes();
draw();
setActiveButton(
group
);
}
/* ==================================================
YEAR NAVIGATION
================================================== */
yearButtons.forEach(
function (button) {
button.addEventListener(
'click',
function () {
const value =
button.classList.contains(
'vh-year-early'
)
? '1896-1964'
: button.textContent.trim();
jumpToGroup(value);
}
);
}
);
/* ==================================================
ANIMATION
================================================== */
function animate(now) {
let elapsed =
(
now -
lastFrame
) /
1000;
lastFrame =
now;
if (elapsed > 0.05) {
elapsed =
0.05;
}
if (!dragging) {
offsetX -=
AUTO_SPEED *
elapsed;
normalizePosition();
draw();
}
animationId =
requestAnimationFrame(
animate
);
}
/* ==================================================
DRAGGING
================================================== */
timelineWindow.addEventListener(
'pointerdown',
function (event) {
if (
event.pointerType ===
'mouse' &&
event.button !== 0
) {
return;
}
dragging =
true;
dragMoved =
false;
pointerStartX =
event.clientX;
previousPointerX =
event.clientX;
timelineWindow.classList.add(
'is-dragging'
);
timelineWindow.setPointerCapture(
event.pointerId
);
event.preventDefault();
}
);
timelineWindow.addEventListener(
'pointermove',
function (event) {
if (!dragging) {
return;
}
const currentX =
event.clientX;
const difference =
currentX -
previousPointerX;
previousPointerX =
currentX;
if (
Math.abs(
currentX -
pointerStartX
) > 4
) {
dragMoved =
true;
}
offsetX +=
difference;
normalizePosition();
draw();
event.preventDefault();
}
);
function endDrag(event) {
if (!dragging) {
return;
}
dragging =
false;
timelineWindow.classList.remove(
'is-dragging'
);
try {
if (
timelineWindow.hasPointerCapture(
event.pointerId
)
) {
timelineWindow.releasePointerCapture(
event.pointerId
);
}
} catch (e) {}
if (dragMoved) {
window.setTimeout(
function () {
dragMoved =
false;
},
180
);
}
}
timelineWindow.addEventListener(
'pointerup',
endDrag
);
timelineWindow.addEventListener(
'pointercancel',
endDrag
);
timelineWindow.addEventListener(
'dragstart',
function (event) {
event.preventDefault();
}
);
/* ==================================================
RESIZE
================================================== */
let resizeTimer;
window.addEventListener(
'resize',
function () {
clearTimeout(
resizeTimer
);
resizeTimer =
setTimeout(
function () {
if (
events.length
) {
buildVirtualTimeline();
}
},
150
);
}
);
/* ==================================================
LOAD ALL TIMELINE DATA
================================================== */
const requests = [];
/*
* First: 1896–1964
*/
requests.push(
fetchEarlyTimeline()
);
/*
* Then: 1965–1977
*/
for (
let year = 1965;
year <= 1977;
year++
) {
requests.push(
fetchYear(year)
);
}
Promise
.all(requests)
.then(
function (results) {
events =
results.flat();
/*
* Ensure chronological order
* between groups.
*/
events.sort(
function (a, b) {
return (
a.year -
b.year
);
}
);
console.log(
'TIMELINE EVENTS:',
events.length
);
console.log(
'EARLY EVENTS:',
events.filter(
function (item) {
return (
item.group ===
'1896-1964'
);
}
).length
);
if (!events.length) {
console.warn(
'No timeline events found.'
);
return;
}
/*
* Start with 1965.
*/
const startIndex =
events.findIndex(
function (item) {
return (
item.year ===
1965
);
}
);
firstIndex =
startIndex >= 0
? startIndex
: 0;
buildVirtualTimeline();
setActiveButton(
'1965'
);
lastFrame =
performance.now();
animationId =
requestAnimationFrame(
animate
);
}
)
.catch(
function (error) {
console.error(
'Timeline loading failed:',
error
);
}
);
}
}
/* =========================================================
RANDOM NECTAR DROP — HOMEPAGE QUOTE
========================================================= */
if (mw.config.get('wgPageName') === 'Main_Page') {
const quoteWrap =
document.getElementById('vh-random-quote-wrap');
const quoteText =
document.getElementById('vh-random-quote');
const quoteSource =
document.getElementById('vh-random-quote-source');
if (quoteWrap && quoteText && quoteSource) {
const API =
'https://vanipedia.org/w/api.php';
const CATEGORY =
'Category:Nectar Drops from Srila Prabhupada';
let nectarPages = [];
let quoteChanging = false;
let currentPage = '';
/* -----------------------------------------
LOAD ALL NECTAR DROP PAGES
----------------------------------------- */
function loadNectarPages(cmcontinue) {
let url =
API +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(CATEGORY) +
'&cmnamespace=0' +
'&cmlimit=max';
if (cmcontinue) {
url +=
'&cmcontinue=' +
encodeURIComponent(cmcontinue);
}
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'HTTP ' + response.status
);
}
return response.json();
})
.then(function(data) {
if (
data.query &&
data.query.categorymembers
) {
nectarPages =
nectarPages.concat(
data.query.categorymembers
);
}
if (
data.continue &&
data.continue.cmcontinue
) {
return loadNectarPages(
data.continue.cmcontinue
);
}
console.log(
'Nectar Drop pages loaded:',
nectarPages.length
);
});
}
/* -----------------------------------------
GET RANDOM NECTAR DROP
----------------------------------------- */
function getRandomNectarDrop() {
if (
quoteChanging ||
!nectarPages.length
) {
return;
}
quoteChanging = true;
let page;
for (
let attempt = 0;
attempt < 20;
attempt++
) {
const candidate =
nectarPages[
Math.floor(
Math.random() *
nectarPages.length
)
];
if (
candidate &&
candidate.title !== currentPage
) {
page = candidate;
break;
}
}
if (!page) {
quoteChanging = false;
return;
}
/* -------------------------------------
GET RAW WIKITEXT
------------------------------------- */
const url =
API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(page.title) +
'&prop=wikitext';
fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
if (
!data.parse ||
!data.parse.wikitext ||
!data.parse.wikitext['*']
) {
throw new Error(
'No wikitext returned.'
);
}
const wikitext =
data.parse.wikitext['*'];
/* ---------------------------------
FIND AUDIOBOX TEMPLATE
--------------------------------- */
const start =
wikitext.indexOf(
'{{Audiobox_NDrops|'
);
if (start === -1) {
quoteChanging = false;
window.setTimeout(
getRandomNectarDrop,
250
);
return;
}
const end =
wikitext.indexOf(
'}}',
start
);
if (end === -1) {
quoteChanging = false;
window.setTimeout(
getRandomNectarDrop,
250
);
return;
}
const template =
wikitext.substring(
start +
'{{Audiobox_NDrops|'.length,
end
);
/* ---------------------------------
SPLIT TEMPLATE PARAMETERS
--------------------------------- */
const parts =
template.split('|');
if (parts.length < 5) {
quoteChanging = false;
window.setTimeout(
getRandomNectarDrop,
250
);
return;
}
/*
* Parameter structure:
*
* 0 = Nectar Drops from...
* 1 = mp3 player
* 2 = QUOTE
* 3 = Vanisource page
* 4 = source label
*/
let quote =
parts[2]
.trim()
.replace(
/^["“]+/,
''
)
.replace(
/["”]+$/,
''
)
.trim();
const vanisourcePage =
parts[3]
.trim()
/* remove wiki-link brackets */
.replace(/^\[\[/, '')
.replace(/\]\]$/, '')
/* if link contains |display text, keep only target */
.split('|')[0]
/* remove namespace prefix */
.replace(/^Vanisource:/i, '')
/* remove any remaining stray brackets/braces */
.replace(/[\[\]\{\}]/g, '')
.trim();
const sourceLabel =
parts[4]
.trim();
if (!quote) {
quoteChanging = false;
window.setTimeout(
getRandomNectarDrop,
250
);
return;
}
/* ---------------------------------
CLEAN SIMPLE WIKITEXT
--------------------------------- */
quote =
quote
.replace(
/'''?/g,
''
)
.replace(
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
'$2'
)
.replace(
/\[\[([^\]]+)\]\]/g,
'$1'
)
.replace(
/\s+/g,
' '
)
.trim();
/* ---------------------------------
SOURCE LINK
--------------------------------- */
const href =
'https://vanisource.org/wiki/' +
vanisourcePage
.replace(/ /g, '_')
.split('/')
.map(encodeURIComponent)
.join('/');
/* ---------------------------------
DISPLAY
--------------------------------- */
currentPage = page.title;
quoteText.classList.add(
'is-changing'
);
quoteSource.classList.add(
'is-changing'
);
window.setTimeout(
function() {
const cleanQuote =
quote
.replace(
/\s*\(?\[\[Vanisource:.*$/i,
''
)
.trim();
quoteText.textContent =
cleanQuote;
quoteSource.innerHTML =
'';
const link =
document.createElement('a');
link.href =
href;
let cleanSource = page.title
.replace(/^\d{6}[a-z]?\s*-\s*/i, '')
.replace(
/\s*-\s*Srila Prabhupada Speaks a Nectar Drop in\s*/i,
', '
)
.trim();
link.textContent =
cleanSource;
quoteSource.appendChild(
link
);
/*
* Reveal only after the first
* random quote is ready.
*/
quoteWrap.style.visibility =
'visible';
requestAnimationFrame(
function() {
quoteText.classList.remove(
'is-changing'
);
quoteSource.classList.remove(
'is-changing'
);
quoteChanging = false;
}
);
},
450
);
})
.catch(function(error) {
console.error(
'Random Nectar Drop failed:',
error
);
quoteChanging = false;
});
}
/* -----------------------------------------
START
----------------------------------------- */
loadNectarPages()
.then(function() {
if (nectarPages.length) {
getRandomNectarDrop();
window.setInterval(
getRandomNectarDrop,
9000
);
}
})
.catch(function(error) {
console.error(
'Nectar Drop category failed:',
error
);
});
}
}
/* Vani Treasure (Featured) */
/* ==========================================================
HOMEPAGE — RANDOM SHORT
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const shortCard =
document.getElementById('vh-random-short');
if (shortCard) {
const API =
'https://vanipedia.org/w/api.php';
const SHORTS_PAGE =
'VanimediaMayapur YouTube SHORTS List';
let shortItems = [];
let currentShort = -1;
/* --------------------------------------------------
LOAD SHORTS LIST
-------------------------------------------------- */
function loadShorts() {
const url =
API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(SHORTS_PAGE) +
'&prop=wikitext';
fetch(url)
.then(function(response) {
return response.json();
})
.then(function(data) {
if (
!data.parse ||
!data.parse.wikitext ||
!data.parse.wikitext['*']
) {
throw new Error(
'Shorts wikitext not found'
);
}
const wiki =
data.parse.wikitext['*'];
/*
* Example:
*
* File:Short_1139.png
* |1139 - 21 August
* |link=https://www.youtube.com/shorts/_Wumpw8SAYs
*/
const regex =
/File:(Short_(\d+)\.(?:png|jpg|jpeg|webp))\s*\|\s*(\d+)\s*-\s*([^|\n]+?)\s*\|\s*link=(https:\/\/www\.youtube\.com\/shorts\/[^\s|\n]+)/gi;
let match;
while (
(match = regex.exec(wiki)) !== null
) {
shortItems.push({
file: match[1],
number: match[3],
date: match[4].trim(),
youtube: match[5].trim()
});
}
console.log(
'Homepage Shorts loaded:',
shortItems.length
);
if (shortItems.length) {
showRandomShort();
}
})
.catch(function(error) {
console.error(
'Homepage Shorts failed:',
error
);
});
}
/* --------------------------------------------------
GET ACTUAL THUMBNAIL URL
-------------------------------------------------- */
function getShortThumbnail(fileName) {
const url =
API +
'?action=query' +
'&format=json' +
'&prop=imageinfo' +
'&iiprop=url' +
'&iiurlwidth=400' +
'&titles=' +
encodeURIComponent(
'File:' + fileName
);
return fetch(url)
.then(function(response) {
return response.json();
})
.then(function(data) {
if (
!data.query ||
!data.query.pages
) {
return '';
}
const pages =
Object.values(
data.query.pages
);
if (
!pages.length ||
!pages[0].imageinfo ||
!pages[0].imageinfo.length
) {
return '';
}
const info =
pages[0].imageinfo[0];
return (
info.thumburl ||
info.url ||
''
);
});
}
/* --------------------------------------------------
DISPLAY RANDOM SHORT
-------------------------------------------------- */
function showRandomShort() {
if (!shortItems.length) {
return;
}
let index;
do {
index =
Math.floor(
Math.random() *
shortItems.length
);
}
while (
shortItems.length > 1 &&
index === currentShort
);
currentShort = index;
const item =
shortItems[index];
const media =
shortCard.querySelector(
'.vh-feature-media'
);
const copy =
shortCard.querySelector(
'.vh-feature-copy'
);
if (!media || !copy) {
return;
}
/* ---------- thumbnail ---------- */
getShortThumbnail(item.file)
.then(function(imageUrl) {
if (!imageUrl) {
return;
}
const preload = new Image();
preload.onload = function() {
media.style.backgroundImage =
'url("' + imageUrl + '")';
};
preload.src = imageUrl;
});
/* ---------- text ---------- */
copy.innerHTML = '';
const type =
document.createElement('small');
type.textContent =
'SHORT';
const title =
document.createElement('strong');
title.textContent =
'Loading title…';
copy.appendChild(type);
copy.appendChild(title);
getShortTitle(item.youtube)
.then(function(actualTitle) {
/*
* Make sure this card has not already
* rotated to another Short.
*/
if (
shortItems[currentShort] !== item
) {
return;
}
let cleanTitle = actualTitle
.replace(/\s*-\s*Prabhupada.*$/i, '')
.trim();
title.textContent =
cleanTitle ||
item.number;
});
/* ---------- click ---------- */
shortCard.onclick =
function() {
window.open(
item.youtube,
'_blank',
'noopener'
);
};
}
/* --------------------------------------------------
START
-------------------------------------------------- */
loadShorts();
/*
* Temporary individual rotation.
*
* Later we'll replace this with the common
* staggered controller for all four cards.
*/
window.setInterval(
function() {
if (shortItems.length) {
showRandomShort();
}
},
10500
);
}
}
/* ==========================================================
HOMEPAGE — RANDOM VIDEO
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const videoCard =
document.getElementById('vh-random-video');
if (videoCard) {
const API =
'https://vanipedia.org/w/api.php';
const CATEGORY =
'Category:1080 English Pages with Videos';
let videoPages = [];
let currentVideo = '';
/* --------------------------------------------------
LOAD VIDEO PAGES
-------------------------------------------------- */
function loadVideoPages(cmcontinue) {
let url =
API +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(CATEGORY) +
'&cmnamespace=0' +
'&cmtype=page' +
'&cmlimit=max';
if (cmcontinue) {
url +=
'&cmcontinue=' +
encodeURIComponent(cmcontinue);
}
return fetch(url)
.then(function(response) {
return response.json();
})
.then(function(data) {
if (
data.query &&
data.query.categorymembers
) {
data.query.categorymembers
.forEach(function(page) {
if (
/^Prabhupada \d{4}\s*-/i
.test(page.title)
) {
videoPages.push(page);
}
});
}
if (
data.continue &&
data.continue.cmcontinue
) {
return loadVideoPages(
data.continue.cmcontinue
);
}
});
}
/* --------------------------------------------------
CHOOSE RANDOM VIDEO
-------------------------------------------------- */
function chooseRandomVideoPage() {
if (!videoPages.length) {
return null;
}
let page;
do {
page =
videoPages[
Math.floor(
Math.random() *
videoPages.length
)
];
} while (
videoPages.length > 1 &&
page.title === currentVideo
);
return page;
}
/* --------------------------------------------------
CLEAN TITLE
-------------------------------------------------- */
function getVideoDisplayTitle(pageTitle) {
return pageTitle
.replace(
/^Prabhupada\s+\d{4}\s*-\s*/i,
''
)
.trim();
}
/* --------------------------------------------------
VANIPEDIA PAGE URL
-------------------------------------------------- */
function getVideoPageUrl(pageTitle) {
return (
'https://vanipedia.org/wiki/' +
encodeURIComponent(
pageTitle.replace(/ /g, '_')
)
.replace(/%2F/g, '/')
.replace(/%3A/g, ':')
);
}
/* --------------------------------------------------
GET YOUTUBE ID FROM PAGE WIKITEXT
-------------------------------------------------- */
function loadVideoInfo(page) {
const url =
API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(page.title) +
'&prop=wikitext';
return fetch(url)
.then(function(response) {
return response.json();
})
.then(function(data) {
if (
!data.parse ||
!data.parse.wikitext ||
!data.parse.wikitext['*']
) {
return null;
}
const wiki =
data.parse.wikitext['*'];
/*
* Example:
* {{youtube_right|rEc1U_lsJ7s|Expand to Ten Million...}}
*/
const match =
wiki.match(
/\{\{youtube_right\s*\|\s*([A-Za-z0-9_-]{11})\s*\|/i
);
const videoId =
match
? match[1]
: '';
const thumbnailUrl =
videoId
? 'https://i.ytimg.com/vi/' +
videoId +
'/hqdefault.jpg'
: '';
return {
pageUrl:
getVideoPageUrl(
page.title
),
videoId:
videoId,
thumbnailUrl:
thumbnailUrl
};
});
}
/* --------------------------------------------------
SHOW RANDOM VIDEO
-------------------------------------------------- */
function showRandomVideo() {
const page =
chooseRandomVideoPage();
if (!page) {
return;
}
currentVideo =
page.title;
const media =
videoCard.querySelector(
'.vh-feature-media'
);
const copy =
videoCard.querySelector(
'.vh-feature-copy'
);
if (!media || !copy) {
return;
}
/* ---------- title ---------- */
copy.innerHTML = '';
const type =
document.createElement('small');
type.textContent =
'VIDEO';
const title =
document.createElement('strong');
title.textContent =
getVideoDisplayTitle(
page.title
);
copy.appendChild(type);
copy.appendChild(title);
/* ---------- thumbnail + click ---------- */
loadVideoInfo(page)
.then(function(info) {
if (
!info ||
currentVideo !== page.title
) {
return;
}
if (info.thumbnailUrl) {
const preload =
new Image();
preload.onload =
function() {
if (
currentVideo ===
page.title
) {
media.style.backgroundImage =
'url("' +
info.thumbnailUrl +
'")';
}
};
preload.src =
info.thumbnailUrl;
}
videoCard.onclick =
function() {
const target =
info.videoId
? 'https://www.youtube.com/watch?v=' +
info.videoId
: info.pageUrl;
window.open(
target,
'_blank',
'noopener'
);
};
})
.catch(function(error) {
console.error(
'Homepage Video failed:',
error
);
});
}
/* --------------------------------------------------
START
-------------------------------------------------- */
loadVideoPages()
.then(function() {
console.log(
'Homepage Video pages loaded:',
videoPages.length
);
if (videoPages.length) {
showRandomVideo();
window.setInterval(
showRandomVideo,
11500
);
}
})
.catch(function(error) {
console.error(
'Homepage Video category failed:',
error
);
});
}
}
/* ==========================================================
HOMEPAGE — RANDOM IMAGE QUOTE
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const imageQuoteCard =
document.getElementById('vh-random-imagequote');
if (imageQuoteCard) {
const IMAGE_API =
'https://vanimedia.org/w/api.php';
const MONTH_CATEGORIES = [
'Category:January Quotes with Images',
'Category:February Quotes with Images',
'Category:March Quotes with Images',
'Category:April Quotes with Images',
'Category:May Quotes with Images',
'Category:June Quotes with Images',
'Category:July Quotes with Images',
'Category:August Quotes with Images',
'Category:September Quotes with Images',
'Category:October Quotes with Images',
'Category:November Quotes with Images',
'Category:December Quotes with Images'
];
let imageQuoteFiles = [];
let currentImageQuote = '';
/* ======================================================
LOAD ONE MONTH CATEGORY
====================================================== */
function loadImageQuoteCategory(
category,
cmcontinue
) {
let url =
IMAGE_API +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(category) +
'&cmnamespace=6' +
'&cmtype=file' +
'&cmlimit=max';
if (cmcontinue) {
url +=
'&cmcontinue=' +
encodeURIComponent(cmcontinue);
}
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'Image Quote category HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
if (
data.query &&
data.query.categorymembers
) {
data.query.categorymembers
.forEach(function(file) {
if (file.title) {
imageQuoteFiles.push(
file
);
}
});
}
if (
data.continue &&
data.continue.cmcontinue
) {
return loadImageQuoteCategory(
category,
data.continue.cmcontinue
);
}
});
}
/* ======================================================
LOAD ALL MONTHS
====================================================== */
function loadAllImageQuotes() {
const jobs =
MONTH_CATEGORIES.map(
function(category) {
return loadImageQuoteCategory(
category
);
}
);
return Promise.all(jobs);
}
/* ======================================================
RANDOM FILE
====================================================== */
function chooseRandomImageQuote() {
if (!imageQuoteFiles.length) {
return null;
}
let file;
do {
file =
imageQuoteFiles[
Math.floor(
Math.random() *
imageQuoteFiles.length
)
];
} while (
imageQuoteFiles.length > 1 &&
file.title === currentImageQuote
);
return file;
}
/* ======================================================
GET THUMBNAIL + FULL IMAGE
====================================================== */
function getImageQuoteInfo(file) {
const url =
IMAGE_API +
'?action=query' +
'&format=json' +
'&prop=imageinfo' +
'&iiprop=url' +
'&iiurlwidth=500' +
'&titles=' +
encodeURIComponent(
file.title
);
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'Image Quote image HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
if (
!data.query ||
!data.query.pages
) {
return null;
}
const page =
Object.values(
data.query.pages
)[0];
if (
!page ||
!page.imageinfo ||
!page.imageinfo.length
) {
return null;
}
const info =
page.imageinfo[0];
const thumbnailUrl =
info.thumburl ||
info.url ||
'';
const fullUrl =
info.url ||
thumbnailUrl;
const pageUrl =
'https://vanimedia.org/wiki/' +
encodeURIComponent(
file.title.replace(
/ /g,
'_'
)
)
.replace(/%2F/g, '/')
.replace(/%3A/g, ':');
return {
thumbnailUrl:
thumbnailUrl,
fullUrl:
fullUrl,
pageUrl:
pageUrl
};
});
}
/* ======================================================
FILENAME → HUMAN DATE
072866 → July 28, 1966
====================================================== */
function getImageQuoteTitle(
fileTitle
) {
const clean =
fileTitle
.replace(
/^File:/i,
''
)
.replace(
/\.(jpg|jpeg|png|webp)$/i,
''
)
.replace(
/\s*Image-quote$/i,
''
)
.trim();
const match =
clean.match(
/^(\d{2})(\d{2})(\d{2})/
);
if (!match) {
return clean;
}
const month =
parseInt(
match[1],
10
);
const day =
parseInt(
match[2],
10
);
const year =
1900 +
parseInt(
match[3],
10
);
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
if (
month < 1 ||
month > 12 ||
day < 1 ||
day > 31
) {
return clean;
}
return (
months[month - 1] +
' ' +
day +
', ' +
year
);
}
/* ======================================================
LIGHTBOX
====================================================== */
function openImageQuoteLightbox(
imageUrl
) {
let lightbox =
document.getElementById(
'vh-imagequote-lightbox'
);
if (!lightbox) {
lightbox =
document.createElement(
'div'
);
lightbox.id =
'vh-imagequote-lightbox';
lightbox.innerHTML =
'<button ' +
'class="vh-imagequote-lightbox-close" ' +
'type="button" ' +
'aria-label="Close">×</button>' +
'<img ' +
'alt="Śrīla Prabhupāda Image Quote">';
document.body.appendChild(
lightbox
);
lightbox.addEventListener(
'mouseleave',
function() {
lightbox.classList.remove(
'is-open'
);
}
);
lightbox.addEventListener(
'click',
function(event) {
if (
event.target ===
lightbox ||
event.target
.classList
.contains(
'vh-imagequote-lightbox-close'
)
) {
lightbox.classList
.remove(
'is-open'
);
}
}
);
document.addEventListener(
'keydown',
function(event) {
if (
event.key ===
'Escape'
) {
lightbox.classList
.remove(
'is-open'
);
}
}
);
}
const image =
lightbox.querySelector(
'img'
);
image.src =
imageUrl;
const cardRect =
imageQuoteCard.getBoundingClientRect();
lightbox.classList.add(
'is-open'
);
requestAnimationFrame(function() {
const popupRect =
lightbox.getBoundingClientRect();
/*
* Place popup immediately to the
* right of the Image Quote card.
*/
let left =
cardRect.right - 240;
let top =
cardRect.top - 220;
/*
* If there is not enough room
* on the right, place it above.
*/
if (
left +
popupRect.width >
window.innerWidth - 10
) {
left =
cardRect.right -
popupRect.width;
top =
cardRect.top -
popupRect.height -
12;
}
/*
* Keep inside viewport.
*/
left =
Math.max(
10,
Math.min(
left,
window.innerWidth -
popupRect.width -
10
)
);
top =
Math.max(
10,
top
);
lightbox.style.left =
left + 'px';
lightbox.style.top =
top + 'px';
});
}
/* ======================================================
DISPLAY RANDOM IMAGE QUOTE
====================================================== */
function showRandomImageQuote() {
const file =
chooseRandomImageQuote();
if (!file) {
return;
}
currentImageQuote =
file.title;
const media =
imageQuoteCard.querySelector(
'.vh-feature-media'
);
const copy =
imageQuoteCard.querySelector(
'.vh-feature-copy'
);
if (!media || !copy) {
return;
}
/*
* Card text
*/
copy.innerHTML = '';
const type =
document.createElement(
'small'
);
type.textContent =
'IMAGE QUOTE';
const title =
document.createElement(
'strong'
);
title.textContent =
getImageQuoteTitle(
file.title
);
copy.appendChild(
type
);
copy.appendChild(
title
);
/*
* Load image
*/
getImageQuoteInfo(
file
)
.then(function(info) {
if (
!info ||
currentImageQuote !==
file.title
) {
return;
}
if (
info.thumbnailUrl
) {
const preload =
new Image();
preload.onload =
function() {
if (
currentImageQuote ===
file.title
) {
media.style
.backgroundImage =
'url("' +
info.thumbnailUrl +
'")';
}
};
preload.src =
info.thumbnailUrl;
}
/*
* Open full image
* in lightbox
*/
imageQuoteCard.onclick =
function() {
if (
info.fullUrl
) {
openImageQuoteLightbox(
info.fullUrl
);
}
};
})
.catch(function(error) {
console.error(
'Homepage Image Quote failed:',
error
);
});
}
/* ======================================================
START
====================================================== */
loadAllImageQuotes()
.then(function() {
console.log(
'Homepage Image Quotes loaded:',
imageQuoteFiles.length
);
if (
imageQuoteFiles.length
) {
showRandomImageQuote();
window.setInterval(
showRandomImageQuote,
9800
);
}
})
.catch(function(error) {
console.error(
'Homepage Image Quote categories failed:',
error
);
});
}
}
/* ==========================================================
HOMEPAGE — RANDOM NECTAR DROP
========================================================== */
if (mw.config.get('wgPageName') === 'Main_Page') {
const nectarCard =
document.getElementById('vh-random-nectar');
if (nectarCard) {
const NECTAR_API =
'https://vanipedia.org/w/api.php';
const NECTAR_CATEGORY =
'Category:Nectar Drops from Srila Prabhupada';
let nectarPages = [];
let currentNectar = '';
let nectarAudio = null;
let nectarLocked = false;
const audioArea =
nectarCard.querySelector(
'.vh-feature-audio'
);
const audioIcon =
nectarCard.querySelector(
'.vh-feature-audio-icon'
);
const nectarWave =
document.createElement(
'div'
);
nectarWave.className =
'vh-nectar-wave';
nectarWave.innerHTML =
'<span></span>' +
'<span></span>' +
'<span></span>' +
'<span></span>' +
'<span></span>' +
'<span></span>' +
'<span></span>';
audioArea.appendChild(
nectarWave
);
const copy =
nectarCard.querySelector(
'.vh-feature-copy'
);
/* ======================================================
LOAD ALL NECTAR DROP PAGES
====================================================== */
function loadNectarPages(
cmcontinue
) {
let url =
NECTAR_API +
'?action=query' +
'&format=json' +
'&list=categorymembers' +
'&cmtitle=' +
encodeURIComponent(
NECTAR_CATEGORY
) +
'&cmnamespace=0' +
'&cmtype=page' +
'&cmlimit=max';
if (cmcontinue) {
url +=
'&cmcontinue=' +
encodeURIComponent(
cmcontinue
);
}
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'Nectar category HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
if (
data.query &&
data.query.categorymembers
) {
data.query.categorymembers
.forEach(function(page) {
if (page.title) {
nectarPages.push(
page
);
}
});
}
if (
data.continue &&
data.continue.cmcontinue
) {
return loadNectarPages(
data.continue.cmcontinue
);
}
});
}
/* ======================================================
CHOOSE RANDOM NECTAR DROP
====================================================== */
function chooseRandomNectar() {
if (!nectarPages.length) {
return null;
}
let page;
do {
page =
nectarPages[
Math.floor(
Math.random() *
nectarPages.length
)
];
} while (
nectarPages.length > 1 &&
page.title === currentNectar
);
return page;
}
/* ======================================================
CREATE SHORT MEANINGFUL TITLE FROM QUOTE
====================================================== */
function getNectarShortText(
quote
) {
if (!quote) {
return 'Śrīla Prabhupāda speaks';
}
let text =
quote
.replace(
/^["“”]+|["“”]+$/g,
''
)
.replace(
/\s+/g,
' '
)
.trim();
/*
* Prefer first sentence when it is
* reasonably short.
*/
const sentenceMatch =
text.match(
/^(.+?[.!?])(?:\s|$)/
);
if (
sentenceMatch &&
sentenceMatch[1].length <= 85
) {
return sentenceMatch[1];
}
/*
* Otherwise take approximately
* the first 70 characters,
* ending at a full word.
*/
if (text.length > 72) {
text =
text.substring(
0,
72
);
const lastSpace =
text.lastIndexOf(
' '
);
if (lastSpace > 45) {
text =
text.substring(
0,
lastSpace
);
}
text += '…';
}
return text;
}
/* ======================================================
GET QUOTE TEXT + AUDIO URL
====================================================== */
function getNectarInfo(
page
) {
const url =
NECTAR_API +
'?action=parse' +
'&format=json' +
'&page=' +
encodeURIComponent(
page.title
) +
'&prop=text';
return fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error(
'Nectar page HTTP ' +
response.status
);
}
return response.json();
})
.then(function(data) {
if (
!data.parse ||
!data.parse.text ||
!data.parse.text['*']
) {
return null;
}
const wrapper =
document.createElement(
'div'
);
wrapper.innerHTML =
data.parse.text['*'];
/*
* Quote
*/
const quoteElement =
wrapper.querySelector(
'.nectardroptext'
);
const quote =
quoteElement
? quoteElement
.textContent
.replace(
/\s+/g,
' '
)
.trim()
: '';
/*
* MP3 source
*/
const source =
wrapper.querySelector(
'audio source'
);
const audioUrl =
source
? source.getAttribute(
'src'
)
: '';
return {
quote:
quote,
audioUrl:
audioUrl
};
});
}
/* ======================================================
RESET PLAY BUTTON
====================================================== */
function resetNectarPlayButton() {
if (audioIcon) {
audioIcon.textContent =
'▶';
}
}
/* ======================================================
PREPARE AUDIO
====================================================== */
function prepareNectarAudio(
audioUrl
) {
/*
* Dispose of previous audio object.
*/
if (nectarAudio) {
nectarAudio.pause();
nectarAudio.src = '';
nectarAudio = null;
}
resetNectarPlayButton();
if (!audioUrl) {
return;
}
nectarAudio =
new Audio(
audioUrl
);
nectarAudio.preload =
'metadata';
/*
* Audio finished naturally.
*/
/*
* Audio finished naturally.
*/
nectarAudio.addEventListener(
'ended',
function() {
resetNectarPlayButton();
nectarWave.classList.remove(
'is-playing'
);
nectarLocked =
false;
}
);
/*
* Audio starts / resumes.
*/
nectarAudio.addEventListener(
'play',
function() {
if (audioIcon) {
audioIcon.textContent =
'❚❚';
}
nectarWave.classList.add(
'is-playing'
);
}
);
/*
* Audio pauses.
*/
nectarAudio.addEventListener(
'pause',
function() {
if (
nectarAudio &&
!nectarAudio.ended &&
audioIcon
) {
audioIcon.textContent =
'▶';
}
nectarWave.classList.remove(
'is-playing'
);
}
);
}
/* ======================================================
PLAY / PAUSE
====================================================== */
function toggleNectarAudio(
event
) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!nectarAudio) {
return;
}
/*
* Playing → pause
*/
if (!nectarAudio.paused) {
nectarAudio.pause();
nectarLocked =
false;
return;
}
/*
* Paused → play/resume
*/
nectarLocked =
true;
nectarAudio
.play()
.catch(function(error) {
console.error(
'Nectar Drop audio failed:',
error
);
});
}
/* ======================================================
DISPLAY RANDOM NECTAR DROP
====================================================== */
function showRandomNectar() {
/*
* Never change the card while
* user is listening or has paused it.
*/
if (nectarLocked) {
return;
}
const page =
chooseRandomNectar();
if (!page) {
return;
}
currentNectar =
page.title;
if (!copy) {
return;
}
copy.innerHTML = '';
const type =
document.createElement(
'small'
);
type.textContent =
'NECTAR DROP';
const title =
document.createElement(
'strong'
);
title.textContent =
'';
copy.appendChild(
type
);
copy.appendChild(
title
);
getNectarInfo(
page
)
.then(function(info) {
if (
!info ||
currentNectar !==
page.title
) {
return;
}
/*
* Display meaningful words
* directly from Nectar Drop.
*/
title.textContent =
getNectarShortText(
info.quote
);
/*
* Prepare MP3.
*/
prepareNectarAudio(
info.audioUrl
);
})
.catch(function(error) {
console.error(
'Homepage Nectar Drop failed:',
error
);
title.textContent =
'Śrīla Prabhupāda speaks';
});
}
/* ======================================================
PLAY BUTTON CLICK
====================================================== */
if (audioArea) {
audioArea.addEventListener(
'click',
toggleNectarAudio
);
}
/* ======================================================
START
====================================================== */
loadNectarPages()
.then(function() {
console.log(
'Homepage Nectar Drops loaded:',
nectarPages.length
);
if (
nectarPages.length
) {
showRandomNectar();
window.setInterval(
function() {
if (!nectarLocked) {
showRandomNectar();
}
},
12700
);
}
})
.catch(function(error) {
console.error(
'Homepage Nectar category failed:',
error
);
});
}
}
const networkMission =
document.getElementById('vh-network-mission');
if (networkMission) {
networkMission.addEventListener(
'click',
function() {
window.location.href =
mw.util.getUrl('Vanipedia_Vision');
}
);
}
/* CLOSE HOMEPAGE WRAPPER */
})();
/* =========================================================
VANIPEDIA FLIPBOOK POPUP
========================================================= */
(function () {
function createFlipbookOverlay() {
if (document.getElementById('vani-flipbook-overlay')) {
return;
}
var overlay = document.createElement('div');
overlay.id = 'vani-flipbook-overlay';
var modal = document.createElement('div');
modal.className = 'vani-flipbook-modal';
var closeButton = document.createElement('button');
closeButton.className = 'vani-flipbook-close';
closeButton.type = 'button';
closeButton.setAttribute('aria-label', 'Close flipbook');
closeButton.innerHTML = '×';
var frameHolder = document.createElement('div');
frameHolder.id = 'vani-flipbook-frame-holder';
modal.appendChild(closeButton);
modal.appendChild(frameHolder);
overlay.appendChild(modal);
document.body.appendChild(overlay);
closeButton.addEventListener('click', function () {
closeFlipbook();
});
overlay.addEventListener('click', function (event) {
if (event.target === overlay) {
closeFlipbook();
}
});
document.addEventListener('keydown', function (event) {
if (
event.key === 'Escape' &&
overlay.classList.contains('is-open')
) {
closeFlipbook();
}
});
}
function openFlipbook(book) {
createFlipbookOverlay();
var overlay =
document.getElementById('vani-flipbook-overlay');
var holder =
document.getElementById('vani-flipbook-frame-holder');
/*
* FIRST make popup visible.
*/
overlay.classList.add('is-open');
document.body.classList.add('vani-flipbook-open');
/*
* Remove any previous iframe.
*/
holder.innerHTML = '';
/*
* Wait until browser has calculated
* the real popup dimensions.
*/
requestAnimationFrame(function () {
requestAnimationFrame(function () {
var frame =
document.createElement('iframe');
frame.id =
'vani-flipbook-frame';
frame.title =
'Vanipedia Flipbook';
frame.setAttribute(
'allowfullscreen',
'true'
);
frame.src =
'https://dev.vanipedia.org/flipbook/?book=' +
encodeURIComponent(book);
holder.appendChild(frame);
});
});
}
function closeFlipbook() {
var overlay =
document.getElementById('vani-flipbook-overlay');
var holder =
document.getElementById('vani-flipbook-frame-holder');
if (!overlay) {
return;
}
overlay.classList.remove('is-open');
document.body.classList.remove('vani-flipbook-open');
if (holder) {
holder.innerHTML = '';
}
}
function initialiseFlipbooks() {
var triggers =
document.querySelectorAll(
'.vani-flipbook-trigger'
);
triggers.forEach(function (trigger) {
if (
trigger.getAttribute(
'data-flipbook-ready'
) === '1'
) {
return;
}
trigger.setAttribute(
'data-flipbook-ready',
'1'
);
trigger.addEventListener(
'click',
function (event) {
event.preventDefault();
var book =
trigger.getAttribute(
'data-book'
);
if (book) {
openFlipbook(book);
}
}
);
});
}
createFlipbookOverlay();
initialiseFlipbooks();
if (
window.mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(function () {
initialiseFlipbooks();
});
}
})();