MediaWiki:Gadget-defaultVisibilityToggles.js: Difference between revisions
No edit summary Tag: Reverted |
No edit summary Tag: Manual revert |
||
| Line 1: | Line 1: | ||
/* jshint undef: true | /* jshint undef: true */ | ||
/* globals $, jQuery, mw, window, getComputedStyle */ | /* globals $, jQuery, mw, window, getComputedStyle */ | ||
(function defaultVisibilityTogglesIIFE() { | |||
"use strict"; | |||
if (window.noDefaultVisibilityToggles) return; | |||
/* == NavBars == */ | |||
var NavigationBarHide = "hide ▲"; | |||
var NavigationBarShow = "show ▼"; | |||
var nbsp = "\u00a0"; | |||
// Check if an element has been activated with a toggle. | |||
// For convenience, this has the side effect of marking the element as having | |||
// a toggle, if it is not already marked. | |||
// Allows the functions to avoid toggleifying elements more than once, which | |||
// can lead to multiple "show" buttons, for instance. | |||
// The argument must be an Element, not a jQuery object. | |||
function checkAndSetToggleified(element) { | |||
if (element.isToggleified) { | |||
return true; | |||
} | |||
element.isToggleified = true; | |||
} | |||
function getToggleCategory(element, defaultCategory) { | |||
if ($(element).find("table").first().is(".translations")) | |||
return "translations"; | |||
var heading = element; | |||
while ((heading = heading.previousElementSibling)) { | |||
// tagName is always uppercase: | |||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName | |||
var num = heading.tagName.match(/H(\d)/); | |||
if (num) | |||
num = Number(num[1]); | |||
else | |||
continue; | |||
if (4 <= num && num <= 6) { | |||
if (heading.getElementsByTagName("span")[1]) | |||
heading = heading.getElementsByTagName("span")[0]; | |||
var text = jQuery(heading).text() | |||
.toLowerCase() | |||
// jQuery's .text() is inconsistent about whitespace: | |||
.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " ") | |||
// remove numbers added by the "Auto-number headings" pref: | |||
.replace(/^[1-9][0-9.]+ ?/, ""); | |||
// Toggle category must be convertible to a valid CSS identifier so | |||
// that it can be used in an id selector in jQuery in | |||
// ToggleCategory.prototype.newSidebarToggle | |||
// in [[MediaWiki:Gadget-VisibilityToggles.js]]. | |||
// Spaces must later be converted to hyphens or underscores. | |||
// Reference: https://drafts.csswg.org/selectors-4/#id-selectors | |||
if (/^[a-zA-Z0-9\s_-]+$/.test(text)) | |||
return text; | |||
else | |||
break; | |||
} else if (num) | |||
break; | |||
} | |||
return defaultCategory; | |||
} | |||
function createNavToggle(navFrame) { | |||
if (checkAndSetToggleified(navFrame)) { | |||
return; | |||
} | |||
var navHead, navContent; | |||
for (var i = 0, children = navFrame.childNodes; i < children.length; ++i) { | |||
var child = children[i]; | |||
if (child.nodeName === "DIV") { | |||
var classList = child.classList; | |||
if (classList.contains("NavHead")) | |||
navHead = child; | |||
if (classList.contains("NavContent")) | |||
navContent = child; | |||
} | |||
} | |||
if (!(navHead && navContent)) | |||
return; | |||
// Step 1, don't react when a subitem is clicked. | |||
$(navHead).find("a").on("click", function (e) { | |||
e.stopPropagation(); | |||
e.stopImmediatePropagation(); | |||
}); | |||
// Step 2, toggle visibility when bar is clicked. | |||
// NOTE This function was chosen due to some funny behaviour in Safari. | |||
var $navToggle = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
$("<span>").addClass("NavToggle").attr("data-nosnippet", "") | |||
.append($navToggle) | |||
.prependTo(navHead); | |||
navHead.style.cursor = "pointer"; | |||
var toggleCategory = $(navFrame).data("toggle-category") | |||
|| getToggleCategory(navFrame, "other boxes"); | |||
navHead.onclick = window.VisibilityToggles.register(toggleCategory, | |||
function show() { | |||
$navToggle.text(NavigationBarHide); | |||
if (navContent) | |||
navContent.style.display = "block"; | |||
}, | |||
function hide() { | |||
$navToggle.text(NavigationBarShow); | |||
if (navContent) | |||
navContent.style.display = "none"; | |||
}); | |||
} | |||
function createNavToggleForInflectionTable(it) { | |||
if (checkAndSetToggleified(it)) { | |||
return; | |||
} | |||
// The table caption is the clickable element | |||
var itCaption = $(it).find("caption").get(0); | |||
// Step 1, don't react when a subitem is clicked. | |||
$(itCaption).find("a").on("click", function (e) { | |||
e.stopPropagation(); | |||
e.stopImmediatePropagation(); | |||
}); | |||
// Step 2, toggle visibility when bar is clicked. | |||
// NOTE This function was chosen due to some funny behaviour in Safari. | |||
var $navToggle = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
$("<span>").addClass("NavToggle").attr("data-nosnippet", "") | |||
.append($navToggle) | |||
.prependTo(itCaption); | |||
itCaption.style.cursor = "pointer"; | |||
var toggleCategory = $(it).data("toggle-category") | |||
|| getToggleCategory(it, "other boxes"); | |||
itCaption.onclick = window.VisibilityToggles.register(toggleCategory, | |||
function show() { | |||
$navToggle.text(NavigationBarHide); | |||
if (it) { | |||
it.classList.remove("inflection-table-collapsed"); | |||
} | |||
}, | |||
function hide() { | |||
$navToggle.text(NavigationBarShow); | |||
if (it) { | |||
it.classList.add("inflection-table-collapsed"); | |||
} | |||
}); | |||
// Check to see if we are on a browser that is known to support | |||
// visibility: collapse, which permits inflection table headings to wrap. | |||
// WebKit needs to be special-cased, as technically it does support | |||
// visibility: collapse, but it just implements it as a synonym for | |||
// visibility: hidden, which is useless. (as of November 2024) | |||
// Yes, I know User-Agent sniffing is so 2004... but WebKit is the new IE | |||
if ( | |||
CSS && CSS.supports && CSS.supports("visibility:collapse") && | |||
// exclude WebKit/Safari, excepting Blink engines which have a frozen WebKit version number | |||
(navigator.userAgent.indexOf("AppleWebKit/") === -1 || navigator.userAgent.indexOf("AppleWebKit/537.36") > -1) | |||
) { | |||
it.classList.remove("no-vc"); | |||
} else { | |||
// Strange behaviour occurs when you set the table caption to nowrap | |||
// The [show/hide] toggle crashes into the caption text | |||
// This spacer element prevents that | |||
$("<span>").addClass("no-vc-spacer").appendTo(itCaption); | |||
} | |||
} | |||
/* ==Hidden Quotes== */ | |||
function setupHiddenQuotes(li) { | |||
if (checkAndSetToggleified(li)) | |||
return; | |||
let HQToggleButton, liComp, dl; | |||
(function () { | function show() { | ||
HQToggleButton.text("quotations ▲"); | |||
$(li).children("ul").show(); | |||
} | |||
function hide() { | |||
HQToggleButton.text("quotations ▼"); | |||
$(li).children("ul").hide(); | |||
} | |||
for (const liComp of li.childNodes) { | |||
// Look at each component of the definition. | |||
if (liComp.tagName === "DL" && !dl) | |||
dl = liComp; | |||
// If we find a ul or dl, we have quotes or example sentences, and thus need a button. | |||
if (liComp.tagName === "UL") { | |||
$(li).children("ul").addClass("wikt-quote-container"); | |||
HQToggleButton = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
$(dl || liComp).before($("<span>").addClass("HQToggle").attr("data-nosnippet", "").append(HQToggleButton).css("margin-left", "5px")); | |||
HQToggleButton.on("click", window.VisibilityToggles.register("quotations", show, hide)); | |||
break; | |||
} | |||
} | |||
} | |||
/* == View Switching == */ | |||
function viewSwitching(rootElement) { | |||
if (checkAndSetToggleified(rootElement)) { | |||
return; | |||
} | |||
var $rootElement = $(rootElement); | |||
var showButtonText = $rootElement.data("vs-showtext") || "more ▼"; | |||
var hideButtonText = $rootElement.data("vs-hidetext") || "less ▲"; | |||
var toSkip = $rootElement.find(".vsSwitcher").find("*"); | |||
var elemsToHide = $rootElement.find(".vsHide").not(toSkip); | |||
var elemsToShow = $rootElement.find(".vsShow").not(toSkip); | |||
// Find the element to place the toggle button in. | |||
var toggleElement = $rootElement.find(".vsToggleElement").not(toSkip).first(); | |||
// The toggleElement becomes clickable in its entirety, but | |||
// we need to prevent this if a contained link is clicked instead. | |||
toggleElement.find("a").on("click", function (e) { | |||
e.stopPropagation(); | |||
e.stopImmediatePropagation(); | |||
}); | |||
// Add the toggle button. | |||
var toggleButton = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
$("<span>").addClass("NavToggle").attr("data-nosnippet", "").append(toggleButton).prependTo(toggleElement); | |||
// Determine the visibility toggle category (for the links in the bar on the left). | |||
var toggleCategory = $rootElement.data("toggle-category"); | |||
if (!toggleCategory) { | |||
var classNames = $rootElement.attr("class").split(/\s+/); | |||
for (var i = 0; i < classNames.length; ++i) { | |||
var className = classNames[i].split("-"); | |||
if (className[0] == "vsToggleCategory") { | |||
toggleCategory = className[1]; | |||
} | |||
} | |||
} | |||
if (!toggleCategory) | |||
toggleCategory = "others"; | |||
// Register the visibility toggle. | |||
toggleElement.css("cursor", "pointer"); | |||
toggleElement.on("click", window.VisibilityToggles.register(toggleCategory, | |||
function show() { | |||
toggleButton.text(hideButtonText); | |||
elemsToShow.hide(); | |||
elemsToHide.show(); | |||
}, | |||
function hide() { | |||
toggleButton.text(showButtonText); | |||
elemsToShow.show(); | |||
elemsToHide.hide(); | |||
})); | |||
} | |||
/* ==List switching== */ | |||
function enableListSwitchGeneric(rootElement) { | |||
if (checkAndSetToggleified(rootElement)) { | |||
return; | |||
} | |||
var $rootElement = $(rootElement); | |||
// Create a toggle button. | |||
var $toggleElement = $("<div>").addClass("list-switcher-element"); | |||
var $navToggle = $("<span>").addClass("NavToggle").attr("data-nosnippet", ""); | |||
var $toggleButton = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
// Add the toggle button to the DOM tree. | |||
$navToggle.append($toggleButton).prependTo($toggleElement); | |||
$toggleElement.insertAfter($rootElement); | |||
$toggleElement.show(); | |||
// Determine the visibility toggle category (for the links in the bar on the | |||
// left). It will either be the value of the "data-toggle-category" | |||
// attribute or will be based on the text of the closest preceding | |||
// fourth-to-sixth-level header. | |||
var toggleCategory = $rootElement.data("toggle-category") | |||
|| getToggleCategory($rootElement[0], "other lists"); | |||
// Determine the text for the $toggleButton. | |||
var showButtonText = "show more ▼"; | |||
var hideButtonText = "show less ▲"; | |||
var numItems; | |||
// special handling for [[Module:collapsible category tree]] | |||
var $categoryTreeTag = $rootElement.children(".CategoryTreeTag"); | |||
if ($categoryTreeTag) { | |||
if ($categoryTreeTag.attr("data-pages-left-over") !== "0") { | |||
// some category members are omitted from the list (MediaWiki limitation) | |||
// just use basic "show more/less" in this case for now, this is a big change | |||
//showButtonText = "show first 200 of " + $categoryTreeTag.attr("data-pages-in-cat") + " ▼"; | |||
//hideButtonText = "show fewer ▲"; | |||
} else { | |||
// all category members are included in the list | |||
numItems = $categoryTreeTag.attr("data-pages-in-cat"); | |||
} | |||
} else { | |||
// standard list-switcher using <li> elements | |||
numItems = $rootElement.find("li").length; | |||
} | |||
if (numItems) { | |||
showButtonText = "show all " + numItems + " ▼"; | |||
hideButtonText = "show fewer ▲"; | |||
} | |||
// Register the visibility toggle. | |||
$toggleElement.on("click", window.VisibilityToggles.register(toggleCategory, | |||
function show() { | |||
$toggleButton.text(hideButtonText); | |||
if (rootElement) { | |||
rootElement.classList.remove("list-switcher-collapsed"); | |||
} | |||
}, | |||
function hide() { | |||
$toggleButton.text(showButtonText); | |||
if (rootElement) { | |||
rootElement.classList.add("list-switcher-collapsed"); | |||
} | |||
})); | |||
// Register a resize observer to see if we need to keep the | |||
// show/hide toggle visible | |||
var termList = rootElement.querySelector(':scope > .term-list'); | |||
if (termList && window.ResizeObserver) { | |||
var resizeObserver = new ResizeObserver(function(entries) { | |||
if (entries[0] && entries[0].contentBoxSize[0]) { | |||
// Work out what the max-height would be, in pixels | |||
// As a hack, this value is stored in the CSS `bottom` | |||
// property, as `max-height` is only in place when | |||
// the list is collapsed, but we need to do this check | |||
// even when the list is not collapsed | |||
var maxHeightPx = parseFloat(getComputedStyle(rootElement).bottom); | |||
// If box height is less than its max height + 20 px, suppress | |||
// collapsibility. The 20 px buffer prevents the situation where | |||
// clicking "show more" expands the box by just a few pixels | |||
if (entries[0].contentBoxSize[0].blockSize <= maxHeightPx + 20) { | |||
$toggleElement.hide(); | |||
if (rootElement.classList.contains("list-switcher-collapsed")) { | |||
rootElement.classList.remove("list-switcher-collapsed"); | |||
rootElement.classList.add("list-switcher-collapsibility-suppressed"); | |||
} | |||
} else { | |||
$toggleElement.show(); | |||
if (rootElement.classList.contains("list-switcher-collapsibility-suppressed")) { | |||
rootElement.classList.remove("list-switcher-collapsibility-suppressed"); | |||
rootElement.classList.add("list-switcher-collapsed"); | |||
} | |||
} | |||
} | |||
}); | |||
resizeObserver.observe(termList); | |||
} | |||
} | |||
// based on [[User:Erutuon/scripts/semhide.js]], [[User:Jberkel/semhide.js]], | |||
// [[User:Ungoliant_MMDCCLXIV/synshide.js]] | |||
function setupNyms(index, dlTag) { | |||
// [[Wiktionary:Semantic relations]] | |||
var relationClasses = [ "synonym", "antonym", "hypernym", "hyponym", "meronym", | |||
"holonym", "troponym", "comeronym", "coordinate-term", | |||
"near-synonym", "imperfective", "perfective", "alternative-form" ]; | |||
var relations = $(dlTag).find("dd > .nyms").get().filter( | |||
function(element) { | |||
return Array.prototype.some.call(element.classList, function (className) { | |||
if (relationClasses.indexOf(className) !== -1) { | |||
element.dataset.relationClass = className; | |||
return true; | |||
} | |||
}); | |||
}); | |||
function setupToggle(elements, category, visibleByDefault) { | |||
if (elements.length === 0) return null; | |||
var toggler = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
var text = elements.map(function (e) { | |||
var linkCount = e.querySelectorAll("span[lang]").length; | |||
return e.dataset.relationClass.replace("-", " ") + | |||
(linkCount > 1 ? "s" : ""); | |||
}).join(", "); | |||
function show() { | |||
toggler.text(text + nbsp + "▲"); | |||
$(dlTag).show(); | |||
$(elements).show(); | |||
} | |||
function hide() { | |||
toggler.text(text + nbsp + "▼"); | |||
if ($(dlTag).children().length === elements.length) { | |||
$(dlTag).hide(); | |||
} else { | |||
$(elements).hide(); | |||
} | |||
} | |||
$(dlTag).before($("<span>") | |||
.addClass("nyms-toggle") | |||
.attr("data-nosnippet", "") | |||
.append(toggler) | |||
.css("margin-left", "5px")); | |||
toggler.click(window.VisibilityToggles.register(category, show, hide, visibleByDefault)); | |||
} | |||
var synonyms = relations.filter(function (e) { | |||
return ["synonym", "antonym", "near-synonym", "coordinate-term", "alternative-form"].indexOf(e.dataset.relationClass) !== -1; | |||
}); | |||
var other = relations.filter(function (e) { return synonyms.indexOf(e) === -1; }); | |||
setupToggle(synonyms, "synonyms", true /* show by default */); | |||
setupToggle(other, "semantic relations"); | |||
} | |||
function setupUsageExampleCollapses(index, dlTag) { | |||
var usexTags = $(dlTag).find("dd > .h-usage-example").get(); | |||
function setupToggle(elements, category, visibleByDefault) { | |||
if (elements.length === 0) return null; | |||
var toggler = $("<a>").attr("role", "button").attr("tabindex", "0"); | |||
function show() { | |||
toggler.text(category + nbsp + "▲"); | |||
$(dlTag).show(); | |||
$(elements).show(); | |||
} | |||
function hide() { | |||
toggler.text(category + nbsp + "▼"); | |||
if ($(dlTag).children().length === elements.length) { | |||
$(dlTag).hide(); | |||
} else { | |||
$(elements).hide(); | |||
} | |||
} | |||
$(dlTag).before($("<span>") | |||
.addClass("nyms-toggle") | |||
.append(toggler) | |||
.css("margin-left", "5px")); | |||
toggler.click(window.VisibilityToggles.register(category, show, hide, visibleByDefault)); | |||
} | |||
var collocations = usexTags.filter(function (e) { | |||
return $(e).hasClass("collocation"); | |||
}); | |||
var usexes = usexTags.filter(function (e) { return collocations.indexOf(e) === -1; }); | |||
setupToggle(usexes, "usage examples", true /* show by default */); | |||
setupToggle(collocations, "collocations", true /* show by default */); | |||
} | |||
window.createNavToggle = createNavToggle; | |||
window.setupHiddenQuotes = setupHiddenQuotes; | |||
window.viewSwitching = viewSwitching; | |||
window.getToggleCategory = getToggleCategory; | |||
/* == Apply four functions defined above == */ | |||
mw.hook("wikipage.content").add(function($content) { | |||
// NavToggles | |||
$(".NavFrame", $content).each(function(){ | |||
createNavToggle(this); | |||
}); | |||
$(".inflection-table-collapsible", $content).each(function(){ | |||
createNavToggleForInflectionTable(this); | |||
}); | |||
// order nyms -> usexes -> quotes, to match the conventional order in entries | |||
// synonyms and such under definitions | |||
// if (mw.config.get("wgNamespaceNumber") === 0) { | |||
$("dl:has(dd > .nyms)", $content).each(setupNyms); | |||
// } | |||
// usage examples and collocations | |||
var namespaceNumber = mw.config.get("wgNamespaceNumber"); | |||
if (window.defaultVisibilityTogglesForUsageExamples) { | |||
if (namespaceNumber === 0) { | |||
$("ol > li dl:has(dd > .h-usage-example)", $content).each(setupUsageExampleCollapses); | |||
} | |||
} | |||
// quotes | |||
if (namespaceNumber === 0 || namespaceNumber === 100 || namespaceNumber === 118) { | |||
// First, find all the ordered lists, i.e. all the series of definitions. | |||
$("ol > li", $content).each(function(){ | |||
setupHiddenQuotes(this); | |||
}); | |||
} | |||
//view switching | |||
$(".vsSwitcher", $content).each(function(){ | |||
viewSwitching(this); | |||
}); | |||
// list switching | |||
$(".list-switcher", $content).each(function () { | |||
enableListSwitchGeneric(this); | |||
}); | |||
}); | |||
jQuery(mw).on("LivePreviewDone", function (ev, sels) { | |||
var ols = jQuery(sels.join(",")).find("ol"); | |||
for (var i = 0; i < ols.length; i++) { | |||
for (var j = 0; j < ols[i].childNodes.length; j++) { | |||
var li = ols[i].childNodes[j]; | |||
if (li.nodeName.toUpperCase() == "LI") { | |||
setupHiddenQuotes(li); | |||
} | |||
} | |||
} | |||
}); | |||
})(); | })(); | ||