MediaWiki:Common.js
From Bahai9
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* globals document, window, URLSearchParams, location, event, console, localStorage,
$, mw, -- Set by MediaWiki */
/* eslint-disable
no-tabs, padded-blocks, no-multiple-empty-lines,
vars-on-top
-- Disabling for readability
*/
/* eslint-disable
no-var, prefer-named-capture-group, require-c-regexp,
object-shorthand,
unicorn/no-document-cookie,
unicorn/prefer-query-selector, unicorn/prefer-includes, unicorn/no-for-loop,
unicorn/prefer-default-parameters
-- Disabling some due to older browser support/lack of ES6+ support
*/
/* eslint indent: ['error', 'tab'] -- Differs from eslint-config-ash-nazg */
/*
CONTENTS
1. Character subset menu
2. Collapsible headings
3. Collapsible lists
4. Toggling table of contents
5. Toggling headings
6. Search category pages
7. Prepopulating user category pages
8. Sister project links (convert to specific page)
9. Hiding category sections when no categories
10. Bahai.org and Phelps' Partial Inventory Tablet cross-references
11. Add traffic statistics link
*/
// MediaWiki vars (use mw.config.get() for some): https://www.mediawiki.org/wiki/Manual:Interface/JavaScript
/* Any JavaScript here will be loaded for all users on every page load. */
'use strict';
/**********************
*** add menu on edit page /for selecting subsets of special characters
*** by [[user:Pathoschild]]
*** - note: must match MediaWiki:Edittools
**********************/
function addCharSubsetMenu () {
var specialchars = document.getElementById('specialchars');
if (specialchars) {
var menu = '<select style="display:inline" onChange="chooseCharSubset(selectedIndex)">';
menu += '<option>Select</option>';
menu += '<option>Ligatures and symbols</option>';
menu += '<option>Accents</option>';
menu += '<option>Tildes</option>';
menu += '<option>Cedillas</option>';
menu += '<option>Diereses</option>';
menu += '<option>Circumflexes</option>';
menu += '<option>Macrons</option>';
menu += '<option>Other diacritics</option>';
menu += '<option>Greek</option>';
menu += '<option>Hebrew</option>';
menu += '<option>Cyrillic</option>';
menu += '<option>IPA</option>';
menu += '</select>';
specialchars.innerHTML = menu + specialchars.innerHTML.replace(/_newline_/gm, '\n');
/* default subset - try to use a cookie some day */
chooseCharSubset(0);
}
}
/**
* Select subsection of special characters.
* @param {int} s
* @returns {void}
*/
function chooseCharSubset (s) {
var l = document.getElementById('specialchars').getElementsByTagName('p');
for (var i = 0; i < l.length; i++) {
l[i].style.display = i === s ? 'inline' : 'none';
l[i].style.visibility = i === s ? 'visible' : 'hidden';
}
}
addCharSubsetMenu(); // addOnloadHook();
// Collapsible headings
var a = $('<a href="javascript:void(0);">collapse</a>').click(function () {
var currentHeading = parseInt($(this).parent().parent()[0].nodeName.match(/\d$/));
var sameOrHigherHeading = 'h' + Array.from({length: 6}, function (x, i) {return i+1;}).slice(0, currentHeading).join(',h');
var method = this.textContent === 'collapse' ? 'hide' : 'show';
$(this).parent().parent().nextUntil(sameOrHigherHeading)[method]();
this.textContent = this.textContent === 'collapse' ? 'show' : 'collapse';
});
$('h1,h2,h3,h4,h5,h6').children('.mw-editsection').append(' [', a, ']');
// add collapsible list if an empty span is present indicating to do so
if ($('.enable_collapsibleList').length || $('.enable_collapsibleList_collapsed').length) {
$('ul,ol').each(function () {
$(this).addClass('collapsibleList');
var li = $(this).find('li:has(ul,ol)').addClass('collapsibleListOpen');
if (!li.find('span').length) {
li.prepend('<span style="user-select: none;"> </span>');
}
});
}
// User:Brettz9 added with some JSLint/XHTML/JSDoc clean-up, optimization/convenience changes, OL support, and beginning of work to allow pre-expansion of lists
/*
MEDIAWIKI LIST NOTES
Nested DL hierarchies do not work properly with Mediawiki (see bug at http://www.mediawiki.org/wiki/Thread:Project:Support_desk/How_to_create_nested_definition_lists%3F )
Resources:
1. http://www.mediawiki.org/wiki/Help:Formatting
2. http://en.wikipedia.org/wiki/Help:List
*/
/*
CollapsibleLists.js
An object allowing lists to dynamically expand and collapse
Original version created by Stephen Morley - http://code.stephenmorley.org/javascript/collapsible-lists/ -
and released under the terms of the CC0 1.0 Universal legal code:
http://creativecommons.org/publicdomain/zero/1.0/legalcode
Ideas
1. Remember expanded states
2. Make DL collapsible via its DD
3. Support collapsing of DL component children (DD and DT)
Todo summary: (see "todo"'s below)
1. Optimize iteration algorithms
2. When #1 is completed, check top-most class of list (or its parent) for `doNotRecurse` and pre-expansion options
*/
// eslint-disable-next-line no-unused-vars -- Might expand
var CollapsibleLists;
(function () {
// Create the CollapsibleLists object
var collapsibleLists,
autoApplyWithNoGlobal = true; // Configure as to whether to auto-apply to lists on the page (without creating a global) or whether to allow manual control via a global
/**
* Opens or closes the list elements of the given type
* within the specified node.
* @private
* @static
* @param {Element} node The node containing the list elements
* @param {'ul'|'ol'} [listType] Type of list; defaults to "ul"
* @returns {int}
*/
function toggle (node, listType) {
listType = listType || 'ul,ol';
// Determine whether to open or close the lists
var index, li,
open = node.className.match(/(^| )collapsibleListClosed( |$)/),
// Loop over the list elements with the node
lists = node.querySelectorAll(listType),
listsLength = lists.length;
for (index = 0; index < listsLength; index++) {
// Find the ancestor list item of this list
li = lists[index];
while (li.nodeName.toLowerCase() !== 'li') {
li = li.parentNode;
}
// Style the list if it is within this node
if (li === node) {
lists[index].style.display = (open ? 'block' : 'none');
}
}
if (listsLength) {
// Remove the current class from the node
node.className = node.className.replace(
/(^| )collapsibleList(Open|Closed)( |$)/, ''
);
// If the node contains lists, set its class
if (listsLength > 0) {
node.className += ' collapsibleList' + (open ? 'Open' : 'Closed');
}
}
return listsLength;
}
/**
* @callback ClickHandler
* @private
* @static
* @param {Event} e The click event
* @returns {void}
*/
/**
* Returns a function that toggles the display status of any
* list elements within the specified node. The parameter is...
* @private
* @static
* @param {Element} node The node containing the list elements
* @returns {ClickHandler}
*/
function createClickListener (node) {
// Return the function
/**
* @type {ClickHandler}
*/
return function (e) {
// Ensure the event object is defined
e = e || window.event;
// Find the list item containing the target of the event
var li = e.target || e.srcElement;
while (li.nodeName.toLowerCase() !== 'li') {
li = li.parentNode;
}
// Toggle the state of the node if it was the target of the event
if (li === node) {
toggle(node);
}
};
}
/**
* @private
* @static
* @param {Event} e The event to prevent
* @returns {void}
*/
function preventDefault (e) {
e.preventDefault();
}
/**
* @private
* @static
* @returns {void}
*/
function preventDefaultIE () {
// eslint-disable-next-line no-restricted-globals -- IE
event.returnValue = false;
}
/**
* @param {boolean} leaveExpanded
* @private
* @static
* @returns {void}
*/
function applyLists (leaveExpanded) {
collapsibleLists.apply(false, leaveExpanded);
}
/**
* Makes sublists of a given list type collapsible.
* @private
* @static
* @param {Element} list The list element
* @param {'ul'|'ol'} [listType] The list type under which sublists should be made collapsible
* @returns {void}
*/
function applySubListsForListType (list, listType) {
listType = listType || 'ul,ol';
// Todo: This is unnecessarily redundant and thus expensive for deeply nested lists;
// should instead check direct children recursively
var subIndex,
subLists = list.getElementsByTagName(listType),
subListsLength = subLists.length;
for (subIndex = 0; subIndex < subListsLength; subIndex++) {
// Tempory fix to at least avoid multiple classes
if (!(/(^| )collapsibleList( |$)/).test(subLists[subIndex].className)) {
subLists[subIndex].className += ' collapsibleList';
}
}
}
collapsibleLists = {
/**
* Makes all lists with the class 'collapsibleList' collapsible.
* @param {boolean} [doNotRecurse] True if sub-lists should not be made collapsible
* @param {boolean} [leaveExpanded] True if list and its sublists should be left pre-expanded
* @returns {void}
*/
apply: function (doNotRecurse, leaveExpanded) {
this.applyForListType(doNotRecurse, leaveExpanded);
},
/**
* Makes lists of the given type with the class 'collapsibleList' collapsible.
* @param {boolean} [doNotRecurse] True if sub-lists should not be made collapsible
* @param {boolean} [leaveExpanded] True if list and its sublists should be left pre-expanded
* @param {'ul'|'ol'} [listType] Type of list; defaults to "ul"
*/
applyForListType: function (doNotRecurse, leaveExpanded, listType) {
listType = listType || 'ul,ol';
// Loop over the lists
var index, list,
// Todo: This is unnecessarily redundant and thus inefficient; should instead
// iterate over direct children
lists = document.querySelectorAll(listType),
listsLength = lists.length,
listPattern = /(^| )collapsibleList( |$)/;
for (index = 0; index < listsLength; index++) {
list = lists[index];
// Check whether this list should be made collapsible
if (listPattern.test(list.className) ||
listPattern.test(list.parentNode.className) // For convenience when used with Mediawiki simple syntax lists (which cannot specify classes on the list)
) {
// Make this list collapsible
this.applyTo(list, true, leaveExpanded);
// Check whether sub-lists should also be made collapsible
// Todo: When iteration algorithm is fixed, check class at top of list (or parent of list) to allow doNotRecurse specification
if (!doNotRecurse) {
// Add the collapsibleList class to the sub-lists
applySubListsForListType(list);
}
}
}
},
/**
* Makes the specified list collapsible.
* @param {Element} node The list element
* @param {boolean} [doNotRecurse] True if sub-lists should not be made collapsible
* @param {boolean} [leaveExpanded] True if list and its sublists should be left pre-expanded
*/
applyTo: function (node, doNotRecurse, leaveExpanded) {
// Loop over the list items within this node
var index,
lis = node.querySelectorAll('li'),
lisLength = lis.length;
for (index = 0; index < lisLength; index++) {
// Todo: When iteration algorithm is fixed, check class at top of list (or parent of list) to allow doNotRecurse specification
// Check whether this list item should be collapsible
if (!doNotRecurse || node === lis[index].parentNode) {
var attachNode = lis[index].querySelector('span');
if (!attachNode) {
continue;
}
// Prevent text from being selected unintentionally
if (attachNode.parentNode.addEventListener) {
attachNode.parentNode.addEventListener('mousedown', preventDefault, false);
} else {
attachNode.parentNode.attachEvent('onselectstart', preventDefaultIE);
}
// Add the click listener
if (attachNode.parentNode.addEventListener) {
attachNode.parentNode.addEventListener('click', createClickListener(lis[index]), false);
} else {
attachNode.parentNode.attachEvent('onclick', createClickListener(lis[index]));
}
// Close the lists within this list item
// Todo: When iteration algorithm is fixed, check class at top of list (or parent of list) to allow expansion via class
if (!leaveExpanded) {
toggle(lis[index]);
}
}
}
}
};
if (autoApplyWithNoGlobal) {
/*
if (window.addEventListener) {
window.addEventListener('DOMContentLoaded', applyLists, false);
}
else {
window.attachEvent('onload', applyLists);
}
*/
applyLists(!$('.enable_collapsibleList_collapsed').length);
} else {
CollapsibleLists = collapsibleLists;
}
}());
/**
* @param {Element} el
* @param {string} newText
* @returns {void}
*/
/*
function changeText (el, newText) {
if (el.firstChild && el.firstChild.nodeValue) {
el.firstChild.nodeValue = newText;
}
}
*/
var t, toc, toggleLink;
try {
t = document.getElementById('toc');
if (t) { // No TOC for pages with no headings
t.style.display = 'block';
toc = t.getElementsByTagName('ul')[0];
toggleLink = document.getElementById('toctogglecheckbox');
// if (tocIsHidden()) {
toggleToc();
// }
}
} catch (error) {
// eslint-disable-next-line no-console -- Error surfacing
console.log('erred', error);
}
/**
* @returns {boolean}
*/
function tocIsHidden () {
return !toc || !toggleLink || window.getComputedStyle(toc).display !== 'block';
}
/**
* @returns {void}
*/
function toggleToc () {
var hidden = tocIsHidden();
if (hidden && document.cookie.indexOf('hidetoc=0') > -1) {
toggleLink.click();
// changeText(toggleLink, tocShowText);
// toc.style.display = 'none';
// eslint-disable-next-line sonarjs/no-duplicated-branches -- Might expand
} else if (!hidden && document.cookie.indexOf('hidetoc=1') > -1) {
toggleLink.click();
// changeText(toggleLink, tocHideText);
// toc.style.display = 'block';
}
}
if (toggleLink) {
toggleLink.addEventListener('click', function () {
var isHidden = tocIsHidden();
document.cookie = isHidden ? 'hidetoc=1' : 'hidetoc=0';
});
}
// </source>
/**
* @param {boolean} forceConceal
* @returns {void}
*/
function toggleHeadings (forceConceal) {
var headings = $('#content h2:not(#mw-toc-heading), #content h3, #content h4, #content h5, #content h6');
var th = $('#toggle-headings');
if (!forceConceal && localStorage.getItem('b9-hide-headings')) {
localStorage.removeItem('b9-hide-headings');
headings.show();
th.text('Hide headings');
} else {
localStorage.setItem('b9-hide-headings', '1');
headings.hide();
th.text('Show headings');
}
}
$(
'<li><br /><a href="javascript:void(0);" id="toggle-headings">Hide headings</a></li>'
).appendTo(
// '#p-tb .vector-menu-content-list'
'.sidebar .navbar-nav'
).click(function () {
toggleHeadings();
});
if (localStorage.getItem('b9-hide-headings')) {
toggleHeadings(true);
}
// Search category pages
// If a category page
if (mw.config.get('wgNamespaceNumber') === 14 && $('#mw-pages').length) {
$('<br><br><form action="/index.php"><label><b>Search category pages</b>: <input id="categoryWorkSearch" /></label>' +
'<input type="submit" value="go"/></form>').submit(function () {
var $el = $('<input type="hidden" name="search" />');
$el.attr({
value: 'incategory:"' + mw.config.get('wgTitle') + '" ' +
$(this).find('#categoryWorkSearch').val()
});
$(this).append($el);
}).appendTo('#mw-pages > p'); // .prependTo('#mw-content-text');
}
// Prepopulating user category pages
var params = new URLSearchParams(location.search);
if (params.has('prepopulate') && params.get('action') === 'edit' && !$('#wpTextbox1').val()) {
var title = params.get('title');
var match = title.match(/User:[^/]*([^&]*)$/);
var text = '[[Category:User' + match[1] + ']]';
$('#wpTextbox1').val(text);
}
// Sister project links (convert to specific page)
// If want these added server-side, could add as header/footer using https://www.mediawiki.org/wiki/Extension:Header_Footer
if (mw.config.get('wgNamespaceNumber') === 0) { // Namespaces: https://www.mediawiki.org/wiki/Extension_default_namespaces
var pageName = mw.config.get('wgPageName');
var encPageName = encodeURIComponent(pageName);
var noUnderscoresPageName = encPageName.replace(/_/g, '+');
// $('a.nav-link:contains("Bahai.media")').prop('href', 'https://bahai.media/index.php?search=' + noUnderscoresPageName + '&title=Special%3ASearch');
// $('a.nav-link:contains("Bahai.works")').prop('href', 'https://bahai.works/index.php?search=' + noUnderscoresPageName + '&title=Special%3ASearch');
// $('a.nav-link:contains("Bahaipedia.org")').prop('href', 'https://bahaipedia.org/index.php?search=' + noUnderscoresPageName + '&title=Special%3ASearch');
// $('a.nav-link:contains("Bahai.org Writings")').prop('href', 'https://www.bahai.org/library/authoritative-texts/search?q=' + noUnderscoresPageName);
$('a.nav-link:contains("Indexes")').prop('href', 'https://bahai-browser.org/indexes/json/?indexTerm=' + noUnderscoresPageName);
$('a.nav-link:contains("Bahai-library")').prop('href', 'https://www.google.com/search?q=site%3Abahai-library.com+' + noUnderscoresPageName);
// $('a.nav-link:contains("Bahai-library tags")').prop('href', 'https://bahai-library.com/tags/' + (pageName === 'Main_Page' ? '' : encodeURI(pageName)));
// $('a.nav-link:contains("Wikipedia")').prop('href', 'https://en.wikipedia.org/?search=' + noUnderscoresPageName + '&title=Special%3ASearch');
// Sister wikis via bahaidata.org
$('.p-wikibase-otherprojects-toggle').hide();
$('.nav-menu-label.other').after(
$('<div id="bahai-wikibase-links" class="nav-item"></div>').append(
$('.sidebar-menu .wb-otherproject-link > a')
)
);
}
/**
* 9. Hide category container when no categories can exist (special pages) ***************************
*
*/
var catlinksElement = document.querySelector('.catlinks-allhidden');
var navElement = document.querySelector('nav.p-navbar.not-collapsible.small.mb-2');
if (catlinksElement && navElement) {
navElement.parentNode.removeChild(navElement);
}
/**
* 10. Bahai.org and Phelps Partial Inventory Tablet cross-references
*/
var wikibaseItemId = mw.config.get('wgWikibaseItemId');
if (wikibaseItemId) {
var getMoreInfoLink = $('<a class="nav-link" href="javascript:void(0);">Show more links</a>');
$('#bahai-wikibase-links').before(
$('<div class="nav-item"></div>').append(getMoreInfoLink)
);
getMoreInfoLink.on('click', function () {
fetch('https://bahai-library.com/' + wikibaseItemId + '?format=json').then(function (resp) {
return resp.json();
}).then(function (json) {
getMoreInfoLink.parent().remove();
[
['tag', 'Bahai-library tags', 'https://bahai-library.com/tag/'],
['phelps', 'Partial Inventory', 'https://bahai-library.com/inventory/']
].forEach(function (info) {
if (!json[info[0]]) {
return;
}
$('#bahai-wikibase-links').before(
$('<div class="nav-item"><a class="nav-link" href="' +
encodeURI(info[2]) + encodeURIComponent(json[info[0]].replaceAll(' ', '_')) +
'">' + info[1] + '</a></div>')
);
});
[
['wikipedia', 'Wikipedia'],
['bahaiOrg', 'Bahai.org']
].forEach(function (info) {
if (!json[info[0]]) {
return;
}
$('#bahai-wikibase-links').before(
$('<div class="nav-item"><a class="nav-link" href="' +
// encodeURI( // Already escaped
json[info[0]] +
//) +
'">' + info[1] + '</a></div>')
);
});
});
});
}
/**
* 11. Add a URL in the footer to the traffic statistics pages
*/
$(function () {
var allowedNamespaces = [0, 14];
var ns = mw.config.get('wgNamespaceNumber');
if (!allowedNamespaces.includes(ns) || mw.config.get('wgAction') !== 'view') return;
var pageTitle = mw.config.get('wgPageName');
var decodedTitle = pageTitle.replace(/_/g, ' ');
var encodedTitle = encodeURIComponent(decodedTitle).replace(/%20/g, '+');
var statsURL = 'https://digitalbahairesources.org/pageview-analysis?website_id=5&titles=%5B%22' +
encodedTitle + '%22%5D&from_year=2024&from_month=7&to_year=2025&to_month=7';
var $link = $('<a>')
.attr('href', statsURL)
.attr('target', '_blank')
.text('📊 View Traffic Statistics')
.css({ marginLeft: '1em' });
var $container = $('#footer-info > div').first();
if ($container.length) {
$container.append($link);
}
});
// if (mw.config.get('wgCategories').includes("Works of Bahá'u'lláh")) {
// var wikibaseItemId = mw.config.get('wgWikibaseItemId');
// mw.loader.using('mediawiki.api').then(function () {
// var api = new mw.Api();
// api.get({
// action: 'expandtemplates',
// prop: 'wikitext',
// text: '{{#property:P50|from=' + wikibaseItemId + '}} ' + '{{#property:P51|from=' + wikibaseItemId + '}}'
// }).done(function (data) {
// var props = data.expandtemplates.wikitext.split(' ');
// var bahaiOrg = props[0],
// inventory = props[1];
// if (bahaiOrg) {
// $('#bahai-wikibase-links').before(
// $('<div class="nav-item"><a class="nav-link" href="' + encodeURI(bahaiOrg) + '">Bahai.org</a></div>')
// );
// }
// if (inventory) {
// // See also http://blog.loomofreality.org/?page_id=252
// var inv = inventory.replace(/</g, '<').replace(/&/g, '&');
// $('#bahai-wikibase-links').before(
// $('<div class="nav-item"><a class="nav-link" href="https://bahai-library.com/inventory/' + inv + '">Inventory ID: ' +
// inv +
// '</a></div>')
// );
// }
// });
// });
// }