MediaWiki:Gadget-VisibilityToggles.js
Jump to navigation
Jump to search
Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.
- Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
- Konqueror and Chrome: click Reload or press F5;
- Opera: clear the cache in Tools → Preferences;
- Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.
- This script lacks a documentation subpage. Please create it.
- This script is a part of the
VisibilityToggles
gadget (edit definitions)- Description (edit): ⧼Gadget-VisibilityToggles⧽
- Useful links: subpage list • links • redirects
/* eslint-env es5, browser, jquery */
/* eslint semi: "error" */
/* jshint esversion: 5, eqeqeq: true */
/* globals $, mw */
/* requires mw.cookie, mw.storage */
(function VisibilityTogglesIIFE () {
"use strict";
// Toggle object that is constructed so that `toggle.status = !toggle.status`
// automatically calls either `toggle.show()` or `toggle.hide()` as appropriate.
// Creating toggle also automatically calls either the show or the hide function.
function Toggle (showFunction, hideFunction) {
this.show = showFunction, this.hide = hideFunction;
}
Toggle.prototype = {
get status () {
return this._status;
},
set status (newStatus) {
if (typeof newStatus !== "boolean")
throw new TypeError("Value of 'status' must be a boolean.");
if (newStatus === this._status)
return;
this._status = newStatus;
if (this._status !== this.toggleCategory.status)
this.toggleCategory.updateToggle(this._status);
if (this._status)
this.show();
else
this.hide();
},
};
/*
* Handles storing a boolean value associated with a `name` stored in
* localStorage under `key`.
*
* The `get` method returns `true`, `false`, or `undefined` (if the storage
* hasn't been tampered with).
* The `set` method only allows setting `true` or `false`.
*/
function BooleanStorage(key, name) {
if (typeof key !== "string")
throw new TypeError("Expected string");
if (!(typeof name === "string" && name !== "")) {
throw new TypeError("Expected non-empty string");
}
this.key = key; // key for localStorage
this.name = name; // name of toggle category
function convertOldCookie(cookie) {
return cookie.split(';')
.filter(function(e) { return e !== ''; })
.reduce(function(memo, currentValue) {
var match = /(.+?)=(\d)/.exec(currentValue); // only to test for temporary =[01] format
if (match) {
memo[match[1]] = Boolean(Number(match[2]));
} else {
memo[currentValue] = true;
}
return memo;
}, {});
}
// Look for cookie in old format.
var cookie = mw.cookie.get(key);
if (cookie !== null) {
this.obj = $.extend(this.obj, convertOldCookie(cookie));
mw.cookie.set(key, null); // Remove cookie.
}
}
BooleanStorage.prototype = {
get: function () {
return this.obj[this.name];
},
set: function (value) {
if (typeof value !== "boolean")
throw new TypeError("Expected boolean");
var obj = this.obj;
if (obj[this.name] !== value) {
obj[this.name] = value;
this.obj = obj;
}
},
// obj allows getting and setting the object version of the stored value.
get obj() {
if (typeof this.rawValue !== "string")
return {};
try {
return JSON.parse(this.rawValue);
} catch (e) {
if (e instanceof SyntaxError) {
return {};
} else {
throw e;
}
}
},
set obj(value) {
// throws TypeError ("cyclic object value")
this.rawValue = JSON.stringify(value);
},
// rawValue allows simple getting and setting of the stringified object.
get rawValue () {
return mw.storage.get(this.key);
},
set rawValue (value) {
return mw.storage.set(this.key, value);
},
};
// This is a version of the actual CSS identifier syntax (described here:
// https://stackoverflow.com/a/2812097), with only ASCII and that must begin
// with an alphabetic character.
var asciiCssIdentifierRegex = /^[a-zA-Z][a-zA-Z0-9_-]+$/;
function ToggleCategory (name, defaultStatus) {
this.name = name;
this.sidebarToggle = this.newSidebarToggle();
this.storage = new BooleanStorage("Visibility", name);
this.status = this.getInitialStatus(defaultStatus);
}
// Have toggle category inherit array methods.
ToggleCategory.prototype = [];
ToggleCategory.prototype.addToggle = function (showFunction, hideFunction) {
var toggle = new Toggle(showFunction, hideFunction);
toggle.toggleCategory = this;
this.push(toggle);
toggle.status = this.status;
return toggle;
};
// Generate an identifier consisting of a lowercase ASCII letter and a random integer.
function randomAsciiCssIdentifier() {
var digits = 9;
var lowCodepoint = "a".codePointAt(0), highCodepoint = "z".codePointAt(0);
return String.fromCodePoint(
lowCodepoint + Math.floor(Math.random() * (highCodepoint - lowCodepoint)))
+ String(Math.floor(Math.random() * Math.pow(10, digits)));
}
function getCssIdentifier(name) {
name = name.replace(/\s+/g, "-");
// Generate a valid ASCII CSS identifier.
if (!asciiCssIdentifierRegex.test(name)) {
// Remove characters that are invalid in an ASCII CSS identifier.
name = name.replace(/^[^a-zA-Z]+/, "").replace(/[^a-zA-Z_-]+/g, "");
if (!asciiCssIdentifierRegex.test(name))
name = randomAsciiCssIdentifier();
}
return name;
}
// Add a new global toggle to the sidebar.
ToggleCategory.prototype.newSidebarToggle = function () {
var name = getCssIdentifier(this.name);
var id = "p-visibility-" + name;
var sidebarToggle = $("#" + id);
if (sidebarToggle.length > 0)
return sidebarToggle;
var listEntry = $("<li>");
if (mw.config.get("skin") === "vector-2022")
listEntry.addClass("mw-list-item");
sidebarToggle = $("<a>", {
id: id,
href: "#visibility-" + this.name,
})
.click((function () {
this.status = !this.status;
this.storage.set(this.status);
return false;
}).bind(this));
listEntry.append(sidebarToggle).appendTo(this.buttons);
return sidebarToggle;
};
// Update the status of the sidebar toggle for the category when all of its
// toggles on the page are toggled one way.
ToggleCategory.prototype.updateToggle = function (status) {
if (this.length > 0 && this.every(function (toggle) { return toggle.status === status; }))
this.status = status;
};
// getInitialStatus is only called when a category is first created.
ToggleCategory.prototype.getInitialStatus = function (defaultStatus) {
function isFragmentSet(name) {
return location.hash.toLowerCase().split("_")[0] === "#" + name.toLowerCase();
}
function isHideCatsSet(name) {
var match = /^.+?\?(?:.*?&)*?hidecats=(.+?)(?:&.*)?$/.exec(location.href);
if (match !== null) {
var hidecats = match[1].split(",");
for (var i = 0; i < hidecats.length; ++i) {
switch (hidecats[i]) {
case name: case "all":
return false;
case "!" + name: case "none":
return true;
}
}
}
return false;
}
function isWiktionaryPreferencesCookieSet() {
return mw.cookie.get("WiktionaryPreferencesShowNav") === "true";
}
// TODO check category-specific cookies
return isFragmentSet(this.name)
|| isHideCatsSet(this.name)
|| isWiktionaryPreferencesCookieSet()
|| (function(storedValue) {
return storedValue !== undefined ? storedValue : Boolean(defaultStatus);
}(this.storage.get()));
};
Object.defineProperties(ToggleCategory.prototype, {
status: {
get: function () {
return this._status;
},
set: function (status) {
if (typeof status !== "boolean")
throw new TypeError("Value of 'status' must be a boolean.");
if (status === this._status)
return;
this._status = status;
// Change the state of all Toggles in the ToggleCategory.
for (var i = 0; i < this.length; i++)
this[i].status = status;
this.sidebarToggle.html((status ? "Hide " : "Show ") + this.name);
},
},
buttons: {
get: function () {
var buttons = $("#p-visibility ul");
if (buttons.length > 0)
return buttons;
buttons = $("<ul>");
// unused var collapsed = mw.cookie.get("vector-nav-p-visibility") === "false";
if (mw.config.get("skin") === "vector-2022") {
/* add to right-hand side ('tools') bar */
var toolbox = $("<div>", {
"class": "vector-menu mw-portlet mw-portlet-visibility",
"id": "p-visibility"
})
.append($('<div id="p-visibility-label" aria-label="" class="vector-menu-heading">Visibility</div>'))
.append($("<div>", { class: "vector-menu-content" }).append(buttons.addClass("vector-menu-content-list")));
$('#vector-page-tools').append(toolbox);
} else {
var toolbox = $("<div>", {
"class": "vector-menu vector-menu-portal portal portlet",
"id": "p-visibility"
})
.append($('<label id="p-visibility-label" aria-label="" class="vector-menu-heading"><span class="vector-menu-heading-label">Visibility</span></label>'))
.append($("<div>", { class: "pBody body vector-menu-content" }).append(buttons));
var insert = document.getElementById("p-lang") || document.getElementById("p-feedback");
if (insert) {
$(insert).before(toolbox);
} else {
var sidebar = document.getElementById("mw-panel") || document.getElementById("column-one");
$(sidebar).append(toolbox);
}
}
return buttons;
}
}
});
function VisibilityToggles () {
// table containing ToggleCategories
this.togglesByCategory = {};
}
// Add a new toggle, adds a Show/Hide category button in the toolbar.
// Returns a function that when called, calls showFunction and hideFunction
// alternately and updates the sidebar toggle for the category if necessary.
VisibilityToggles.prototype.register = function (category, showFunction, hideFunction, defaultStatus) {
if (!(typeof category === "string" && category !== ""))
return;
var toggle = this.addToggleCategory(category, defaultStatus)
.addToggle(showFunction, hideFunction);
return function () {
toggle.status = !toggle.status;
};
};
VisibilityToggles.prototype.addToggleCategory = function (name, defaultStatus) {
return (this.togglesByCategory[name] = this.togglesByCategory[name] || new ToggleCategory(name, defaultStatus));
};
window.alternativeVisibilityToggles = new VisibilityToggles();
window.VisibilityToggles = window.alternativeVisibilityToggles;
})();