MediaWiki:Gadget-VisibilityToggles.js

/* 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 = $(""); sidebarToggle = $("", {			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 = $(""); var collapsed = mw.cookie.get("vector-nav-p-visibility") === "false"; var toolbox = $(" ", {					"class": "vector-menu vector-menu-portal portal portlet",					"id": "p-visibility"				}) .append($(' Visibility ')) .append($(" ", { 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;

});