MediaWiki:Gadget-VisibilityToggles.js: difference between revisions

From Wiktionary, the free dictionary
Jump to navigation Jump to search
Content deleted Content added
at least in Vector, sidebar items seem to use nav tag now
try using same classes as in other vector menus (and this code doesn't work for other skins...)
Line 261: Line 261:
var collapsed = mw.cookie.get("vector-nav-p-visibility") === "false";
var collapsed = mw.cookie.get("vector-nav-p-visibility") === "false";
var toolbox = $("<nav>", {
var toolbox = $("<nav>", {
"class": "portal portlet " + (collapsed ? "collapsed" : "expanded"),
"class": "vector-menu vector-menu-portal portal",
"id": "p-visibility"
"id": "p-visibility"
})
})

Revision as of 22:14, 26 August 2020

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

})();