Přeskočit na obsah

Wikipedista:Dvorapa/tools.js

Z Wikipedie, otevřené encyklopedie

Poznámka: Po uložení musíte vyprázdnit mezipaměť vašeho prohlížeče, jinak změny neuvidíte.

/* <onlyinclude> __NOEDITSECTION__
'''Mikroudělátka''' k vložení do uživatelského skriptu (na stránce [[Speciální:Moje stránka/common.js|common.js]]). Pokud chcete využít některé z následujících mikroudělátek, '''následujte návod [[#Instalace]]'''.

Většina mikroudělátek pochází od [[Wikipedista:Mormegil|Mormegila]] ([[Wikipedista:Mormegil/tools.js.help|zdroj]]), některá bohužel již nejsou funkční, postupně ale budou vylepšována a opravována.
</onlyinclude>

****************************************************************************
****************************************************************************
**                                                                        **
** Dokumentaci si můžete přečíst na [[Wikipedista:Dvorapa/tools.js/doc]]. **
**                                                                        **
****************************************************************************
****************************************************************************

<onlyinclude>
== Instalace ==
Pokud chcete jakékoliv z následujících mikroudělátek používat, musíte nejprve na začátek [[Speciální:Moje stránka/common.js|svého souboru ''common.js'']] vložit odkaz na '''tento soubor'''. K tomu slouží následující řádek kódu:
<pre>
importScript("Wikipedista:Dvorapa/tools.js");
</pre>

Dále si tam vložte řádky podle toho, která mikroudělátka chcete použít (viz „Použití“ u každého mikroudělátka):

__TOC__
</onlyinclude><pre> */
// TODO: nahradit následující funkce mw.* funkcemi
// najít element s nadpisem stránky
function findFirstHeading() {
	var eContent = document.getElementById('content');
	if (eContent === null) return null;
	var elems = eContent.getElementsByTagName("h1");
	if (elems.length === 0) return null;
	return elems[0];
}
// zjistit nadpis stránky
function getPagetitle() {
	var eHeading = findFirstHeading();
	if (eHeading === null) return "";
	var title = eHeading.innerHTML;
	// TODO: vyřešit stránky, kde se nečte článek (editace, přesouvání, …)
	return title;
}
// získat (enkódované) lokální URL nějaké stránky (obdoba localurl:)
function getLocalURL(text) {
	text = text.substring(0, 1).toUpperCase() + text.substring(1);
	return "/wiki/" + encodeURIComponent(text.replace(/ /g, "_")).replace(/%2f/gi, "/");
}
// Appends a new tab.
// Převzato z [[commons:MediaWiki:Extra-tabs.js]].
function appendTab(url, name) {
	var na = document.createElement('a');
	na.setAttribute('href', url);
	var txt = document.createTextNode(name);
	na.appendChild(txt);
	var li = document.createElement('li');
	li.appendChild(na);
	// Grab the element we want to append the tab and append the tab to it.
	var c1 = document.getElementById(mw.config.get('skin') === 'vector' ? 'p-cactions' : 'column-one');
	if (!c1) return;
	var tabs = c1.getElementsByTagName('div')[0].getElementsByTagName('ul')[0];
	tabs.appendChild(li);
}
/* </pre><onlyinclude>
== Tlačítka shrnutí ==
; Funkce
: Pod editační okno přidá panel s tlačítky pro vložení často používaných shrnutí editace (převzato částečně z udělátka na [[:sk:MediaWiki:Gadget-edit-summaries.js|skwiki]] a převážně z udělátka na [[:he:מדיה_ויקי:Gadget-Summarieslist.js|hewiki]].
; Použití
:Základní:
 jQuery(function($) { addSummaryToolbar(); });
:Vlastní:
 jQuery(function($) {
 	addSummaryToolbar(
 		["vlastní", {"title": "ahoj", "content": "Vloží ahoj"}, {"title": "svět", "content": "Vloží svět"},
 		"další", {"title": "ahoj světe", "content": "Vloží ahoj světe"}],
 		["diskuse", {"title": "re", "content": "Vloží re"}],
 		["ahoj", "re"]
 	);
 });
; Parametry
:* První parametr obsahuje nadpisy a seznamy dvojic {"title": "shrnutí", "content": "popisek"} oddělené čárkou.
:* Druhý parametr může být buď prázdný řetězec, nebo nadpisy a seznamy dvojic pro diskusní stránky.
:* Třetí parametr může být buď prázdný řetězec, nebo seznam shrnutí, při jejichž vložení se automaticky zaškrtne tlačítko malé editace. Při vložení jiného než zde uvedeného shrnutí se znovu samo odškrtne.
</onlyinclude><pre> */
function addSummaryToolbar(main, talk, minor) {
	var installed = false;

	function installSummary($summaryBox, smallArea) {
		if (installed) return;
		installed = true;

		function addSummary() {
			var summary = $(this).data('summary');
			var text = summary.title;
			var sum = $summaryBox,
				curr = sum.val();
			var comma = curr.length > 0 && curr.charAt(curr.length - 2) != "/";
			sum.val(curr + (comma ? ', ' : '') + text).trigger('input');
			$("#wpMinoredit").prop('checked', summary.minor || $.inArray(text, minorSummaries) + 1);
		}

		var summaries = window.summaries || [];
		if (main === ("" || undefined)) {
			summaries = summaries.concat([
				"malé",
				{"title": "překlepy", "content": "Oprava překlepů"},
				{"title": "odkazy", "content": "Úprava odkazů na jiné články Wikipedie"},
				{"title": "kategorie", "content": "Úprava kategorií"},
				{"title": "externí odkazy", "content": "Úprava externích odkazů"},
				"běžné",
				{"title": "pravopis", "content": "Oprava pravopisu"},
				{"title": "gramatika", "content": "Oprava gramatiky"},
				{"title": "typografie", "content": "Oprava typografie"},
				{"title": "formulace", "content": "Úprava formulace"},
				{"title": "aktualizace", "content": "Aktualizace údajů"},
				{"title": "rozšíření", "content": "Rozšíření článku"},
				"technické",
				{"title": "obrázek", "content": "Přidání, změna, smazání obrázku" },
				{"title": "infobox", "content": "Vložení/úprava infoboxu"},
				{"title": "šablona", "content": "Vložení šablony"}
			]);
		} else {
			summaries = summaries.concat(main);
		}
		if (mw.config.get('wgNamespaceNumber') % 2 == 1 || mw.config.get('wgNamespaceNumber') == 4) {
			if (talk === ("" || undefined)) {

				summaries = summaries.concat([
					"diskuse",
					{"title": "odpověď", "content": "Odpověď"},
					{"title": "návrh", "content": "Návrh"},
					{"title": "dotaz", "content": "Dotaz"}
				]);

			} else {

				summaries = summaries.concat(talk);

			}
		}
		var minorSummaries = [];
		if (minor === ("" || undefined)) {
			minorSummaries = ["překlepy", "odkazy", "kategorie", "externí odkazy"];
		} else {
			minorSummaries = minor;
		}
		var div = $("<div>", {
				id: "summariesList"
			})
			.css({
				width: (smallArea ? "100%" : "65%"),
				padding: "2px",
				fontSize: "85%",
				lineHeight: "18px"
			});

		for (var i = 0; i < summaries.length; i++) {
			var summary = summaries[i];
			var summaryButton = $('<span>').css({
				marginLeft: '0.4em',
				whiteSpace: 'nowrap'
			}).data({
				summary: summary
			});
			if (typeof summary == "string") {
				div.append(smallArea ? '<br>' : ' '); //allow text wrap here
				summaryButton.text(summary + ':');
			} else {
				summaryButton.html('&nbsp' + summary.title + '&nbsp')
					.addClass('clickable-edit-summary')
					.attr('title', summary.content || '')
					.css({
						'background-color': '#f9f9f9',
						border: 'dotted 1px #708090',
						cursor: 'pointer'
					})
					.click(addSummary);
				if (smallArea) div.append(' ');
			}
			div.append(summaryButton);
		}

		$summaryBox.after(div);
	}

	if ($.inArray(mw.config.get('wgAction'), ['edit', 'submit']) + 1) {
		installSummary($('.editOptions #wpSummary'), false);
	}

	mw.hook('ve.saveDialog.stateChanged').add(function() {
		var target = ve.init.target;
		var $summaryBox = target.saveDialog.$body.find('.ve-ui-mwSaveDialog-summary textarea');
		installSummary($summaryBox, true);
	});

}
/* </pre><onlyinclude>
== Odkazy na oblíbené stránky ==
; Funkce
: Do horní lišty přidá odkazy na oblíbené stránky.
; Použití
:Základní:
 jQuery(function($) { addLinktoolbar([["Portál:Historie", "Portál"], ["Wikipedie:Pod lípou", "Pod lípou"]]); });
:Pokročilé:
 jQuery(function($) { addLinktoolbar([["Wikipedie:Žádost o práva správce", "RfA"], ["Wikipedie:Hlasování o smazání", "VfD"]], "Speciální:Poslední změny", "Pracovní", "p-tb", "t-upload"); });
; Parametry
:* První parametr obsahuje seznam dvojic ["odkaz na stránku", "zobrazený text odkazu"] oddělených čárkou. Pokud je místo dvojice prázdná položka "", vloží se na její místo oddělovač.
:* Druhý parametr může být buď prázdný řetězec, nebo název stránky, na které se pouze mají tyto odkazy zobrazit.
:* Třetí parametr může být buď prázdný řetězec, nebo nadpis lišty
:* Čtvrtý parametr může být buď prázdný řetězec, nebo ID jiného prvku, na začátek kterého se mají odkazy vložit.
:* Pátý parametr může být buď prázdný řetězec, nebo ID jiného prvku, před který se mají odkazy vložit.
</onlyinclude><pre> */
function addLinktoolbar(items, page, caption, parent, next) {
	if (page !== ("" || undefined) && getPagetitle() != page) return;
	if (parent === ("" || undefined)) parent = 'p-personal';
	if (next === ("" || undefined)) next = 'pt-preferences';
	if (caption !== ("" || undefined)) {
		mw.util.addPortletLink(
			parent,
			getPagetitle(),
			caption,
			'pt-caption',
			caption,
			null,
			'#' + next
		);
	}
	for (var i = 0; i < items.length; i++) {
		mw.util.addPortletLink(
			parent,
			getLocalURL(items[i][0]),
			items[i][1],
			'pt-' + items[i][1],
			items[i][1],
			null,
			'#' + next
		);
	}
}
/* </pre><onlyinclude>
== Oblíbená interwiki ==
; Funkce
: Přeuspořádá interwiki odkazy tak, aby vybrané jazyky byly navrchu a případně buď ještě zvýrazní vybrané jazyky nebo skryje odkazy na ostatní jazyky.
; Použití
 jQuery(function($) { reorderInterwiki(['en', 'de', 'sk'], false); });
; Parametry
:* První parametr obsahuje seznam jazykových kódů cizojazyčných Wikipedií oddělených čárkou.
:* Druhý parametr obsahuje <code>true</code> pokud se mají zobrazovat ''pouze'' vybrané jazyky nebo <code>false</code> pokud se mají zobrazovat i ostatní a vybrané se mají jen zobrazovat navrchu a tučně.
</onlyinclude><pre> */
function reorderInterwiki(priorityLanguages, removeOthers) {
	langBox = document.getElementById('p-lang');
	if (!langBox) return;
	langList = langBox.getElementsByTagName('ul');
	if (!langList) return;
	langList = langList[0];
	langItems = langList.getElementsByTagName('li');
	priorityList = [];
	for (var l = 0; l < priorityLanguages.length; l++) {
		var reLanguageMatch = new RegExp('(^|\\s)interwiki-' + priorityLanguages[l] + '(\\s|$)');
		for (var i = 0; i < langItems.length; i++) {
			var item = langItems[i];
			if (reLanguageMatch.test(item.className)) {
				langList.removeChild(item);
				priorityList.push(item);
				break;
			}
		}
	}
	if (removeOthers) {
		while (langList.hasChildNodes()) {
			langList.removeChild(langList.childNodes[0]);
		}
	}
	if (langList.hasChildNodes()) {
		var firstNode = langList.childNodes[0];
		for (var i = 0; i < priorityList.length; i++) {
			langList.insertBefore(priorityList[i], firstNode);
			priorityList[i].style.fontWeight = 'bold';
		}
	} else {
		for (var i = 0; i < priorityList.length; i++) {
			langList.appendChild(priorityList[i]);
		}
	}
}
/* </pre><onlyinclude>
== Dodatečné záložky ==
'''{{Námitka}} Sloučit s ''Odkazy na oblíbené stránky'''''
; Funkce
: Přidá některé užitečné záložky: U anonymů odkaz na WHOIS, u neexistujících a zamčených stránek odkaz na log
; Použití
 jQuery(function($) { addAdditionalTabs(); });
</onlyinclude><pre> */
// TODO: zkontrolovat funkčnost
function addAdditionalTabs() {
	var title = mw.config.get('wgPageName');
	var username = null;
	var addWhois = false,
		addLog = false;
	var ns = mw.config.get('wgCanonicalNamespace');
	if (ns === 'User' || ns === 'User_talk') {
		// uživatelská stránka nebo diskuse (příp. podstránka)
		username = title;
		addWhois = true;
		addLog = true;
	} else if (ns === 'Special' && mw.config.get('wgCanonicalSpecialPageName') == 'Contributions') {
		// příspěvky uživatele
		for (var i = 0; i < document.forms.length; ++i) {
			var form = document.forms[i];
			if (form.elements['target']) username = form.elements['target'].value;
			if (username) break;
		}
		if (!username) return;
		username = 'User:' + username;
		addWhois = true;
	} else if (ns !== 'Special') {
		// normální, nespeciální stránka
		if (!document.getElementById('ca-history') || !document.getElementById('ca-edit')) {
			// neexistující nebo zamčená stránka – přidat odkaz na log
			appendTab('/w/index.php?title=Special:Log&page=' + title, "Log");
		}
		return;
	} else return;
	username = username.substring(username.indexOf(':') + 1);
	username = username.replace(/\/.*$/, '');
	if (addLog) appendTab('/w/index.php?title=Special:Log&page=User:' + username, "Log");
	if (username.match(/([0-9]{1,3}\.){3}[0-9]{1,3}/)) {
		// anonymní uživatel
		if (addWhois) appendTab("http://toolserver.org/~chm/whois.php?ip=" + username, "Whois");
	}
}
/* </pre><onlyinclude>
== Odkaz na index ==
'''{{Námitka}} Sloučit s ''Odkazy na oblíbené stránky'''''
; Funkce
: Na každou stránku přidá odkaz do stránky [[Speciální:Všechny stránky]] zobrazující okolí aktuální stránky.
; Použití
 jQuery(function($) { addIndexLink(); });
</onlyinclude><pre> */
// TODO: zkontrolovat funkčnost
function addIndexLink() {
	if (mw.config.get('wgCanonicalNamespace') === "Special") return;
	var pageName = mw.config.get('wgPageName');
	appendTab(getLocalURL('Special:Allpages/' + pageName.substring(0, pageName.length - 1)), 'Index');
}
/* </pre><onlyinclude>
== Odkazy na jiné Wikipedie při chybějícím interwiki ==
'''{{Zamítnuto}} Pravděpodobně nefunkční'''
; Funkce
: U stránky bez interwiki zobrazí odkazy na stejně pojmenované stránky na vybraných cizojazyčných Wikipediích.
; Použití
 jQuery(function($) { showDefaultInterwiki(['cs', 'en']); });
; Parametry
: Buď seznam jazykových kódů cizojazyčných Wikipedií oddělených čárkou, nebo <code>null</code> pro nějaké výchozí
</onlyinclude><pre> */
// TODO: zkontrolovat funkčnost
function ShowDefaultInterwiki(langs) {
	if (!langs || langs.length == 0) {
		langs = ['cs', 'en', 'de', 'sk', 'pl', 'fr'];
	}
	// stránky, které už interwiki mají
	var langBox = document.getElementById('p-lang');
	if (!langBox || document.getElementById('wbc-linkToItem-link')) return;
	// bez editací, historie atd., s výjimkou situace, kdy stránka dosud ani neexistuje (a ošetřit NS MediaWiki)
	if (!document.getElementById('t-cite') && document.getElementById('ca-history') && mw.config.get('wgNamespaceNumber') !== 8) return;
	if (!langBox) {
		langBox = document.createElement('div');
		langBox.id = 'p-lang';
		langBox.className = mw.config.get('skin') === 'vector' ? 'portal' : 'portlet';
		var caption = document.createElement('h5');
		caption.appendChild(document.createTextNode('totéž jinde'));
		langBox.appendChild(caption);
	}
	var langBody = document.createElement('div');
	langBody.className = mw.config.get('skin') === 'vector' ? 'body' : 'pBody';
	langBox.appendChild(langBody);
	var langList = document.createElement('ul');
	langBody.appendChild(langList);
	for (var i = 0; i < langs.length; ++i) {
		var lang = langs[i];
		if (lang == mw.config.get('wgContentLanguage')) continue;
		var item = document.createElement('li');
		item.className = 'interwiki-' + lang;
		langList.appendChild(item);
		var link = document.createElement('a');
		var ns = mw.config.get('wgCanonicalNamespace');
		if (ns.length > 0) ns += ':';
		var title = mw.config.get('wgTitle');
		if (ns === 'Special') title = mw.config.get('wgCanonicalSpecialPageName');
		link.href = '//' + lang + '.wikipedia.org' + getLocalURL(ns + title);
		link.appendChild(document.createTextNode(lang));
		item.appendChild(link);
	}
	var columnOne = document.getElementById(mw.config.get('skin') === 'vector' ? 'mw-panel' : 'column-one');
	columnOne.appendChild(langBox);
}
/* </pre><onlyinclude>
== Chytré sledované stránky ==
; Autor
: [[:en:User:UncleDouggie]]
; Funkce
: Ke sledovaným stránkám přidá další možnosti úpravy
; Použití
 jQuery(function($) { smartWatchlist(); });
; Parametry
: Viz komentáře v kódu
</onlyinclude><pre> */
// TODO: zkontrolovat funkčnost
/** Smart watchlist
 *
 * Provides ability to selectively hide and/or highlight changes in a user's watchlist display.
 * Author: [[User:UncleDouggie]]
 *
 */
// Extend jQuery to add a simple color picker optimized for our use
function smartWatchlist() {

	// works on any display element
	$.fn.swlActivateColorPicker = function(callback) {
		if (this.length > 0 && !$colorPalette) {
			constructPalette();
		}
		return this.each(function() {
			attachColorPicker(this, callback);
		});
	};

	$.fn.swlDeactivateColorPicker = function() {
		return this.each(function() {
			deattachColorPicker(this);
		});
	};

	// set background color of elements using the palette within this class
	$.fn.swlSetColor = function(paletteIndex) {
		return this.each(function() {
			setColor(this, paletteIndex);
		});
	};

	var colorPickerOwner;
	var $colorPalette = null;
	var paletteVisible = false;
	var onChangeCallback = null; // should be able to vary for each color picker using a subclosure (not today)

	var constructPalette = function() {
		$colorPalette = $("<div />")
			.css({
				width: '97px',
				position: 'absolute',
				border: '1px solid #0000bf',
				'background-color': '#f2f2f2',
				padding: '1px'
			});

		// add each color swatch to the pallete
		$.each(colors, function(i) {
			$("<div>&nbsp;</div>").attr("flag", i)
				.css({
					height: '12px',
					width: '12px',
					border: '1px solid #000',
					margin: '1px',
					float: 'left',
					cursor: 'pointer',
					'line-height': '12px',
					'background-color': "#" + this
				})
				.bind("click", function() {
					changeColor($(this).attr("flag"), $(this).css("background-color"))
				})
				.bind("mouseover", function() {
					$(this).css("border-color", "#598FEF");
				})
				.bind("mouseout", function() {
					$(this).css("border-color", "#000");
				})
				.appendTo($colorPalette);
		});
		$("body").append($colorPalette);
		$colorPalette.hide();
	};

	var attachColorPicker = function(element, callback) {
		onChangeCallback = callback;
		$(element)
			.css({
				border: '1px solid #303030',
				cursor: 'pointer'
			})
			.bind("click", togglePalette);
	};

	var deattachColorPicker = function(element) {
		if ($colorPalette) {
			$(element)
				.css({
					border: 'none', // should restore previous value
					cursor: 'default' // should restore previous value
				})
				.unbind("click", togglePalette);
			hidePalette();
		}
	};

	var setColor = function(element, paletteIndex) {
		$(element).css({
			'background-color': '#' + colors[paletteIndex]
		});
		var bright = brightness(colors[paletteIndex]);
		if (bright < 128) {
			$(element).css("color", "#ffffff"); // white text on dark background
		} else {
			$(element).css("color", "");
		}
	};

	var checkMouse = function(event) {

		// check if the click was on the palette or on the colorPickerOwner
		var selectorParent = $(event.target).parents($colorPalette).length;
		if (event.target == $colorPalette[0] || event.target == colorPickerOwner || selectorParent > 0) {
			return;
		}
		hidePalette();
	};

	var togglePalette = function() {
		colorPickerOwner = this;
		paletteVisible ? hidePalette() : showPalette();
	};

	var hidePalette = function() {
		$(document).unbind("mousedown", checkMouse);
		$colorPalette.hide();
		paletteVisible = false;
	};

	var showPalette = function() {
		$colorPalette
			.css({
				top: $(colorPickerOwner).offset().top + ($(colorPickerOwner).outerHeight()),
				left: $(colorPickerOwner).offset().left
			})
			.show();

		//bind close event handler
		$(document).bind("mousedown", checkMouse);
		paletteVisible = true;
	};

	var changeColor = function(paletteIndex, newColor) {
		setColor(colorPickerOwner, paletteIndex);
		hidePalette();
		if (typeof(onChangeCallback) === "function") {
			onChangeCallback.call(colorPickerOwner, paletteIndex);
		}
	};

	var brightness = function(hexColor) {
		// returns brightness value from 0 to 255
		// algorithm from http://www.w3.org/TR/AERT

		var c_r = parseInt(hexColor.substr(0, 2), 16);
		var c_g = parseInt(hexColor.substr(2, 2), 16);
		var c_b = parseInt(hexColor.substr(4, 2), 16);

		return ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;
	};

	var colors = [
		'ffffff', 'ffffbd', 'bdffc2', 'bdf7ff', 'b3d6f9', 'ffbdfa',
		'feb88a', 'ffff66', 'a3fe8a', '8afcfe', 'c1bdff', 'ff80e9',
		'ff7f00', 'ffd733', '39ff33', '33fffd', '0ea7dd', 'cf33ff',
		'db0000', 'e0b820', '0edd1f', '0ba7bf', '3377ff', 'a60edd',
		'990c00', '997500', '0c9900', '008499', '1a0edd', '800099',
		'743436', '737434', '347440', '346674', '1b0099', '743472'
	];
}

/** Smart watchlist settings
 *
 * All settings are grouped together to support save, load, undo, import and export.
 * Child objects are read from local storage or created on the fly.
 * Structure of the settings object:
 *
 * settings: {
 *    controls: {},
 *       Used for control of the GUI and meta data about the settings object.
 *       Not subject to undo or import operations, but it is saved, loaded and exported.
 *
 *    userCategories: [ (displayed category names in menu order, 1 based with no gaps)
 *       1: {
 *          key: category key,
 *          name: category display name
 *       },
 *       2: ...
 *    ],
 *    nextCategoryKey: 1 (monotonically increasing key to link page categories with display names)
 *    rebuildCategoriesOnUndo: "no" or "rebuild" (optimization for undo)
 *
 *    wikiList: [ (in display order when sorted by wiki)
 *       0: {
 *          domain: wiki domain (e.g., "en.wikipedia.org")
 *          displayName: "English Wikipedia"
 *       },
 *       1: ...
 *    ],
 *    wikis: {
 *       wiki domain 1: {
 *          watchlistToken: [  // not included for home wiki/account
 *             0: { token: tokenID,
 *                  userName: username on remote wiki }
 *             1: ...
 *          ],
 *          active: boolean,
 *          expanded: boolen,
 *          lastLoad: time,
 *          pages {  // contains only pages with settings, not everything on a watchlist
 *             pageID1: {
 *                category: category key,
 *                patrolled: revision ID,
 *                flag: page flag key,
 *                hiddenSections: {
 *                   section 1 title: date hidden,
 *                   ...
 *                   }
 *                hiddenRevs: {
 *                   revID1: date hidden,
 *                   ...
 *                }
 *             },
 *             pageID2: ...
 *          },
 *          users {
 *             username1: {
 *                flag: user flag key,
 *                hidden: date hidden
 *             },
 *             username2: ...
 *          }
 *       },
 *       wiki domain 2: ...
 *    }
 * }
 */

// create a closure so the methods aren't global but we can still directly reference them
(function() {

	// global hooks for event handler callbacks into functions within the closure scope
	SmartWatchlist = {
		changeDisplayedCategory: function() {
			changeDisplayedCategory.apply(this, arguments);
		},
		changePageCategory: function() {
			changePageCategory.apply(this, arguments);
		},
		hideRev: function() {
			hideRev.apply(this, arguments);
		},
		patrolRev: function() {
			patrolRev.apply(this, arguments);
		},
		hideUser: function() {
			hideUser.apply(this, arguments);
		},
		processOptionCheckbox: function() {
			processOptionCheckbox.apply(this, arguments);
		},
		clearSettings: function() {
			clearSettings.apply(this, arguments);
		},
		undo: function() {
			undo.apply(this, arguments);
		},
		setupCategories: function() {
			if (setupCategories) {
				setupCategories.apply(this, arguments);
			} else {
				alert("Category editor did not load. Try reloading the page.");
			}
		}
	};

	var settings = {};
	var lastSettings = [];
	var maxSettingsSize = 2000000;
	var maxUndo = 100; // dynamically updated
	var maxSortLevels = 4;

	// for local storage - use separate settings for each wiki user account
	var storageKey = "SmartWatchlist." + mw.config.get('wgUserName');
	var storage = null;

	var initialize = function() {

		// check for local storage availability
		try {
			if (typeof(localStorage) === "object" && typeof(JSON) === "object") {
				storage = localStorage;
			}
		} catch (e) {} // ignore error in FF 3.6 with dom.storage.enabled=false

		readLocalStorage(); // load saved user settings
		initSettings();
		createSettingsPanel();

		// build menu to change the category of a page
		var $categoryMenuTemplate = $constructCategoryMenu("no meta")
			// no attributes other than onChange allowed so the menu can be rebuilt in setupCategories()!
			.attr("onChange", "javascript:SmartWatchlist.changePageCategory(this, value);");

		var lastPageID = null;
		var rowsProcessed = 0;

		// process each displayed change row
		$("table.mw-enhanced-rc tr").each(function() {

			rowsProcessed++;
			var $tr = $(this);
			var $td = $tr.find("td:last-child");
			var isHeader = false;

			// check if this is the header for an expandable list of changes
			if ($tr.find(".mw-changeslist-expanded").length > 0) {
				isHeader = true;
				lastPageID = null; // start of a new page section
			}

			/* Parse IDs from the second link. The link text can be of the following forms:
			     1. "n changes" - used on a header row for a collapsable list of changes
				 2. "cur" - an individual change within a list of changes to the same page
				 3. "diff" - single change with no header row 
				 4. "talk" - deleted revision. No page ID is present on such a row. */

			var $secondLink = $td.find("a:eq(1)"); // get second <a> tag in the cell
			var href = $secondLink.attr("href");
			var linkText = $secondLink.text();
			var pageID = href.replace(/.*&curid=/, "").replace(/&.*/, "");
			var revID = href.replace(/.*&oldid=/, "").replace(/&.*/, "");
			var user = $td.find(".mw-userlink").text();

			// check if we were able to parse the page ID
			if (!isNaN(parseInt(pageID))) {
				lastPageID = pageID;
			}
			// check for a deleted revision
			else if ($td.find(".history-deleted").length > 0 && lastPageID) {
				pageID = lastPageID; // use page ID from the previous row in the same page, if any
			}
			// unable to determine type of row
			else {
				pageID = null;
				if (console) {
					console.log("SmartWatchlist: unable to parse row " + $td.text());
				}
			}

			if (pageID) {

				$tr.attr({
					pageID: pageID,
					wiki: document.domain
				});

				// check if we were able to parse the rev ID and have an individual change row
				if (!isNaN(parseInt(revID)) &&
					(linkText == "cur" || linkText == "diff")) {

					// add the hide change link
					$tr.attr("revID", revID);
					var $revLink = $("<a/>", {
						href: "javascript:SmartWatchlist.hideRev('" + pageID + "', '" + revID + "');",
						title: "Hide this change",
						text: "hide change"
					});
					$td.append($("<span/>")
						.addClass("swlRevisionButton")
						.append(" [").append($revLink).append("]")
					);

					// add the patrol prior changes link
					var $patrolLink = $("<a/>", {
						href: "javascript:SmartWatchlist.patrolRev('" + pageID + "', '" + revID + "');",
						title: "Hide previous changes",
						text: "patrol"
					});
					$td.append($("<span/>")
						.addClass("swlRevisionButton")
						.append(" [").append($patrolLink).append("]")
					);
				}

				// check if this is the top-level row for a page
				if (isHeader || linkText == "diff") {

					// add the category menu with the current page category pre-selected
					$newMenu = $categoryMenuTemplate.clone();
					$td.prepend($newMenu);

					// add the page attribute to the link to the page to support highlighting specific pages
					$td.find("a:eq(0)") // get first <a> tag in the cell
						.attr({
							pageID: pageID,
							wiki: document.domain
						})
						.addClass("swlPageTitleLink");
				}
			}

			// check if we parsed a user for an individual change row
			if (user && !isHeader) {

				// mark change row for possible hiding/flagging
				$tr.attr("wpUser", user);
				if (!$tr.attr("wiki")) {
					$tr.attr("wiki", document.domain);
				}

				// add the hide user link
				var $hideUserLink = $("<a/>", {
					href: "javascript:SmartWatchlist.hideUser('" + user + "');",
					title: "Hide changes by " + user + " on all pages",
					text: "hide user"
				});
				$td.append($("<span/>")
					.addClass("swlHideUserButton")
					.append(" [").append($hideUserLink).append("]")
				);
			}
		}); // close each()

		// set the user attribute for each username link to support highlighting specific users
		$(".mw-userlink").each(function() {
			var $userLink = $(this);
			$userLink.attr({
					wiki: document.domain,
					wpUser: $userLink.text()
				})
				.addClass("swlUserLink");
		});

		initDisplayControls();

		// restore last displayed category and apply display settings
		changeDisplayedCategory(
			selectCategoryMenu($("#swlSettingsPanelCategorySelector"), getSetting("controls", "displayedCategory")));

		// check if we were able to do anything
		if (rowsProcessed == 0) {
			$("#SmartWatchlistOptions")
				.append($("<p/>", {
						text: 'To use Smart Watchlist, enable "enhanced recent changes" in your user preferences.'
					})
					.css("color", "#cc00ff")
				);
		}
	};

	var initDisplayControls = function() {
		// set visibility of buttons and pulldowns shown on each change row
		$(".swlOptionCheckbox").each(function() {
			$checkbox = $(this);

			// restore saved checkbox setting
			$checkbox.attr("checked", getSetting("controls", [$checkbox.attr("controlsProperty")]));

			// apply checkbox value to buttons
			processOptionCheckbox(this);
		});
	};

	// if the desired category exists, pre-select it in the menu
	// otherwise, fallback to the default selection
	var selectCategoryMenu = function($selector, category) {

		// check if page category has been deleted
		if (typeof(category) === "undefined") {
			$selector.attr("selectedIndex", "0"); // fallback to first option
		} else {
			// attempt to use set page category
			$selector.val(category);
			if ($selector.val() == null) {
				// desired category not in the menu, fallback to first option
				$selector.attr("selectedIndex", "0");
			}
		}
		return $selector.val(); // return actual category selected
	};

	// called when the displayed category menu setting is changed
	var changeDisplayedCategory = function(category) {
		setSetting("controls", "displayedCategory", category);
		applySettings();
		writeLocalStorage();
	};

	// called when the category for a page is changed
	var changePageCategory = function(td, category) {

		var $tr = $(td.parentNode.parentNode);
		var pageID = $tr.attr("pageID");
		var wiki = $tr.attr("wiki");

		// convert category to a number if possible
		if (typeof(category) === "string") {
			var intCategory = parseInt(category);
			if (!isNaN(intCategory)) {
				category = intCategory;
			}
		}

		// update category selection menus for all other instances of the page
		$('tr[wiki="' + document.domain + '"][pageID="' + pageID + '"] select').val(category);

		// update settings
		snapshotSettings("change page category");

		if (category == "uncategorized") {
			deleteSetting("wikis", document.domain, "pages", pageID, "category")
		} else {
			setSetting("wikis", document.domain, "pages", pageID, "category", category);
		}
		writeLocalStorage();

		// hide the page immediately if auto refresh
		applySettings();
	};

	// callback for "hide change"
	var hideRev = function(pageID, revID) {

		var mode = getSetting("controls", "displayedCategory");

		// hide the rows unless displaying everything currently
		if (mode != "all+") {
			var $tr = $('tr[wiki="' + document.domain + '"][revID="' + revID + '"]'); // retrieve individual change row
			hideElements($tr);
			suppressHeaders();
		}

		// update settings
		snapshotSettings("hide change");
		if (mode == "hide") {
			deleteSetting("wikis", document.domain, "pages", pageID, "hiddenRevs", revID); // unhide
		} else {
			setSetting("wikis", document.domain, "pages", pageID, "hiddenRevs", revID, new Date()); // hide
		}
		writeLocalStorage();
	};

	// callback for "patrol"
	var patrolRev = function(pageID, revID) {

		var mode = getSetting("controls", "displayedCategory");

		// hide the rows unless displaying everything currently
		if (mode != "all+") {
			var $tr = $('tr[wiki="' + document.domain + '"][pageID="' + pageID + '"]').filter(function() { // filter all rows for the page
				var rowRevID = $(this).attr("revID");
				return (rowRevID <= revID);
			});
			hideElements($tr);
			suppressHeaders();
		}

		// update settings
		snapshotSettings("patrol action");
		setSetting("wikis", document.domain, "pages", pageID, "patrolled", revID);
		writeLocalStorage();
	};

	// callback for "hide user"
	var hideUser = function(user) {

		var mode = getSetting("controls", "displayedCategory");

		// hide the rows unless displaying everything currently
		if (mode != "all+") {
			var $tr = $('tr[wiki="' + document.domain + '"][wpUser="' + user + '"]'); // retrieve all changes by user
			hideElements($tr);
			suppressHeaders();
		}

		// update settings
		snapshotSettings("hide user");
		if (mode == "hide") {
			deleteSetting("wikis", document.domain, "users", user, "hide"); // unhide
		} else {
			setSetting("wikis", document.domain, "users", user, "hide", new Date()); // hide
		}
		writeLocalStorage();
	};

	// toggle the state of a given class of user interface elements
	var processOptionCheckbox = function(checkbox) {
		var $checkbox = $(checkbox);
		var $elements = $("." + $checkbox.attr("controlledClass"));
		if (checkbox.checked) {
			if ($checkbox.hasClass("swlColorPickerControl")) {
				$elements
					.attr("onClick", "javascript:return false;") // disable links so color picker can activate
					.swlActivateColorPicker(setFlag);
			} else {
				$elements.show();
			}
		} else {
			if ($checkbox.hasClass("swlColorPickerControl")) {
				$elements
					.attr("onClick", "") // re-enable links
					.swlDeactivateColorPicker();
			} else {
				$elements.hide();
			}
		}
		setSetting("controls", $checkbox.attr("controlsProperty"), checkbox.checked);
		writeLocalStorage();
	};

	// callback from the color picker to flag a user or page
	var setFlag = function(flag) {

		$this = $(this); // element to be flagged
		var $tr = $this.parents("tr[wiki]");
		var wiki = $tr.attr("wiki");
		var idLabel;
		var settingPath;
		var $idElement;

		if ($this.hasClass("swlUserLink")) {
			idLabel = "wpUser";
			$idElement = $this;
			settingPath = "users";
		} else {
			idLabel = "pageID";
			$idElement = $tr;
			settingPath = "pages";
		}

		var id = $idElement.attr(idLabel);

		if (typeof(id) === "string") {
			snapshotSettings("highlight");

			// update the color on all other instances of the element
			$('a[wiki="' + wiki + '"][' + idLabel + '="' + id + '"]').swlSetColor(flag);

			// update settings
			flag = parseInt(flag);
			if (!isNaN(flag) && flag > 0) {
				setSetting("wikis", wiki, settingPath, id, "flag", flag);
			} else {
				deleteSetting("wikis", wiki, settingPath, id, "flag");
			}
			writeLocalStorage();
		}
	};

	// hide header rows that don't have any displayed changes
	var suppressHeaders = function() {

		// process all change list tables (page headers + changes)
		var $tables = $("table.mw-enhanced-rc");
		$tables.each(function(index) {

			var $table = $(this);

			// check if this is a header table with a following table
			if ($table.filter(":has(.mw-changeslist-expanded)").length > 0 &&
				index + 1 < $tables.length) {

				// check if the following table has visible changes
				var $visibleRows = $tables.filter(":eq(" + (index + 1) + ")")
					.find("tr")
					.not(".swlHidden");

				if ($visibleRows.length == 0) {
					hideElements($table);
				}
			}
		});
	};

	// hide a set of jQuery elements and apply our own class 
	// to support header suppression and later unhiding
	var hideElements = function($elements) {
		$elements.hide();
		$elements.addClass("swlHidden");
	};

	// reinitialize displayed content using current settings
	var applySettings = function() {

		var displayedCategory = getSetting("controls", "displayedCategory");

		// show all changes, including heading tables
		$(".swlHidden").each(function() {
			var $element = $(this);
			$element.show()
			$element.removeClass("swlHidden");
		});

		if (displayedCategory != "all+" && displayedCategory != "hide") { // XXX should showing these be a new option?

			// hide changes by set users
			$('tr[wiki="' + document.domain + '"][wpUser]').each(function() {
				var $tr = $(this);
				if (getSetting("wikis", document.domain, "users", $tr.attr("wpUser"), "hide")) {
					hideElements($tr);
				}
			});
		}

		// process each change row
		$('tr[wiki="' + document.domain + '"][pageID]').each(function() {
			var $tr = $(this);
			var pageID = $tr.attr("pageID");
			var revID = $tr.attr("revID");
			var pageCategory = getSetting("wikis", document.domain, "pages", pageID, "category");
			var pageFlag = getSetting("wikis", document.domain, "pages", pageID, "flag");

			// check if there is a page category menu on the row
			var $select = $tr.find('select');
			if ($select.length == 1) {

				// select proper item in the menu
				var newCategoryKey = selectCategoryMenu($select, pageCategory);

				// reset page category if the current category has been deleted
				if (pageCategory && pageCategory != newCategoryKey) {
					deleteSetting("wikis", document.domain, "pages", pageID, "category");
					pageCategory = newCategoryKey;
				}
			}

			// check if change should be hidden
			// XXX should we show changes by hidden users when in "hidden" display mode? Maybe a new option.
			var visible;

			if (displayedCategory == "all+") {
				visible = true;
			} else if (revID &&
				(getSetting("wikis", document.domain, "pages", pageID, "hiddenRevs", revID) || // specific revision is hidden
					getSetting("wikis", document.domain, "pages", pageID, "patrolled") >= revID // revision has been patrolled
				)) {
				visible = false;
			}
			// check if page is hidden
			else if (pageCategory == "hide" && displayedCategory != "hide") {
				visible = false;
			} else if (displayedCategory == "all") {
				visible = true;
			}
			// check for no category
			else if (displayedCategory == "uncategorized") {
				if (pageCategory) {
					visible = false;
				} else {
					visible = true;
				}
			}
			// check if page is flagged
			else if (displayedCategory == "flag" && typeof(pageFlag) !== "undefined") {
				visible = true;
			}
			// check for selected category
			else if (pageCategory && displayedCategory == pageCategory) {
				visible = true;
			} else {
				visible = false;
			}

			if (!visible) {
				hideElements($tr);
			}
		});

		// hide changes to unknown pages if not displaying all pages
		if (displayedCategory != "all+" && displayedCategory != "all" && displayedCategory != "uncategorized") {
			hideElements($("table.mw-enhanced-rc tr").not('[pageID]'));
		}

		// decorate user links
		$(".mw-userlink").each(function() {
			var $userLink = $(this);
			var user = $userLink.attr("wpUser");
			var flag = getSetting("wikis", document.domain, "users", user, "flag");
			if (typeof(flag) == "number") {
				$userLink.swlSetColor(flag);
			} else {
				$userLink.swlSetColor(0);
			}
		});

		// decorate page titles
		$('a[pageID]').each(function() {
			var $pageTitleLink = $(this);
			var flag = getSetting("wikis", document.domain, "pages", [$pageTitleLink.attr("pageID")], "flag");
			if (typeof(flag) == "number") {
				$pageTitleLink.swlSetColor(flag);
			} else {
				$pageTitleLink.swlSetColor(0);
			}
		});

		suppressHeaders();
	};

	// add smart watchlist settings panel below the standard watchlist options panel
	var createSettingsPanel = function() {

		// construct panel column 1
		var $column1 = $("<td />").attr("valign", "top")
			.append(
				$("<input>", {
					type: "checkbox",
					"class": "swlOptionCheckbox",
					controlledClass: "swlRevisionButton",
					controlsProperty: "showRevisionButtons",
					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"
				})
			)
			.append("Enable hide/patrol change buttons")
			.append("<br />")
			.append(
				$("<input>", {
					type: "checkbox",
					"class": "swlOptionCheckbox",
					controlledClass: "swlHideUserButton",
					controlsProperty: "showUserButtons",
					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"
				})
			)
			.append("Enable hide user buttons")
			.append("<br />")
			.append(
				$("<input>", {
					type: "checkbox",
					"class": "swlOptionCheckbox swlColorPickerControl",
					controlledClass: "swlUserLink",
					controlsProperty: "showUserColorPickers",
					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"
				})
			)
			.append("Assign user highlight colors")
			.append("<br />")
			.append(
				$("<input>", {
					type: "checkbox",
					"class": "swlOptionCheckbox swlColorPickerControl",
					controlledClass: "swlPageTitleLink",
					controlsProperty: "showPageColorPickers",
					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"
				})
			)
			.append("Assign page highlight colors")
			.append("<br />")
			.append(
				$("<input>", {
					type: "checkbox",
					"class": "swlOptionCheckbox",
					controlledClass: "swlPageCategoryMenu",
					controlsProperty: "showPageCategoryButtons",
					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"
				})
			)
			.append("Assign page categories");

		// construct panel column 2
		var $column2 = $("<div />")
			.attr("style", "padding-left: 25pt;")
			.append(
				$("<div />").attr("align", "center")
				.append(
					$("<input />", {
						type: "button",
						onClick: "javascript:SmartWatchlist.clearSettings();",
						title: "Reset all page and user settings and remove all custom categories",
						value: "Clear settings"
					})
				)
				.append("&nbsp;&nbsp;")
				.append(
					$("<input />", {
						type: "button",
						onClick: "javascript:SmartWatchlist.setupCategories();",
						title: "Create, change and delete custom category names",
						value: "Setup categories"
					})
				)
				.append("&nbsp;&nbsp;")
				.append(
					$("<input />", {
						type: "button",
						id: "swlUndoButton",
						onClick: "javascript:SmartWatchlist.undo();",
						title: "Nothing to undo",
						disabled: "disabled",
						value: "Undo"
					})
				)
				.append("<p />")
				.append("Display pages in:&nbsp;")
				.append(
					$constructCategoryMenu("meta")
					// no attributes other than onChange allowed so the menu can be rebuild in setupCategories()!
					.attr("onChange", "javascript:SmartWatchlist.changeDisplayedCategory(value);")
				)
			);

		$sortPanel = $("<div />").attr("align", "right")
			.append("Sort order:&nbsp;");

		for (var i = 0; i < maxSortLevels; i++) {
			$sortPanel
				.append($constructSortMenu().attr("selectedIndex", i))
				.append("<br />");
			if (i == 0) {
				$sortPanel.append("(not yet)&nbsp;&nbsp;");
			}
		}

		// construct panel column 3
		var $column3 = $("<div />")
			.attr("style", "padding-left: 25pt;")
			.append($sortPanel);

		// construct main settings panel
		$("#mw-watchlist-options")
			.after(
				$("<fieldset />", {
					id: "SmartWatchlistOptions"
				})
				.append(
					$("<legend />", {
						text: "Smart watchlist settings"
					})
				)
				.append(
					$("<table />")
					.append(
						$("<tr />")
						.append($column1)
						.append(
							$("<td />", {
								valign: "top"
							})
							.append($column2)
						)
						.append(
							$("<td />", {
								valign: "top"
							})
							.append($column3)
						)
					)
				)
			);

		if (!storage) {
			$("#SmartWatchlistOptions")
				.append(
					$("<p />", {
						text: "Your browser does not support saving settings to local storage. " +
							"Items hidden or highlighted will not be retained after reloading the page."
					})
					.css("color", "red")
				);
		}
	};

	// construct a page category menu
	var $constructCategoryMenu = function(metaOptionString) {

		var $selector =
			$("<select />", {
				"class": "namespaceselector swlCategoryMenu",
				withMeta: metaOptionString // flag so the menu can be rebuilt in setupCategories()
			});

		if (metaOptionString == "meta") {
			// for updating the displayed category selection
			$selector.attr("id", "swlSettingsPanelCategorySelector");
		} else {
			// for hiding/showing page category menus
			$selector.addClass("swlPageCategoryMenu");
		}

		// create default category, must be first in the menu!!!
		var categories = [{
			value: "uncategorized",
			text: "uncategorized"
		}];

		// add user categories, if any
		var userCategories = getSetting("userCategories");
		if (typeof(userCategories) === "object") {
			for (var i = 0; i < userCategories.length && userCategories[i]; i++) {
				var key = userCategories[i].key;
				if (typeof(key) !== "number") {
					alert("Smart watchlist user category definitions are corrupt. You will need to clear your settings. Sorry.");
					break;
				} else {
					categories.push({
						value: userCategories[i].key,
						text: userCategories[i].name
					})
				}
			}
		}

		// add special categories to settings menu
		if (metaOptionString == "meta") {
			categories.push({
				value: "all",
				text: "all except hidden"
			}, {
				value: "flag",
				text: "highlighted"
			});
		}

		categories.push({
			value: "hide",
			text: "hidden"
		});

		if (metaOptionString == "meta") {
			categories.push({
				value: "all+",
				text: "everything"
			});
		}

		// construct all <option> elements
		for (var i in categories) {
			$selector.append($("<option />", categories[i]));
		}
		return $selector;
	};

	// construct a page category menu
	var $constructSortMenu = function() {

		var $selector =
			$("<select />", {
				"class": "namespaceselector swlSortMenu"
			});

		var sortCriteria = [{
			value: "wiki",
			text: "Wiki"
		}, {
			value: "title",
			text: "Title"
		}, {
			value: "timeDec",
			text: "Time (newest first)"
		}, {
			value: "timeInc",
			text: "Time (oldest first)"
		}, {
			value: "risk",
			text: "Vandalism risk"
		}, {
			value: "namespace",
			text: "Namespace"
		}, {
			value: "flagPage",
			text: "Highlighted pages"
		}, {
			value: "flagUser",
			text: "Highlighted users"
		}];

		// construct all <option> elements
		for (var i in sortCriteria) {
			$selector.append($("<option />", sortCriteria[i]));
		}
		return $selector;
	};

	// save settings for later undo
	var snapshotSettings = function(currentAction, rebuildOption) {

		if (typeof(rebuildOption) === "undefined") {
			rebuildOption = "no";
		}
		setSetting("rebuildCategoriesOnUndo", rebuildOption);

		var settingsClone = $.extend(true, {}, settings);
		lastSettings.push(settingsClone);
		while (lastSettings.length > maxUndo) {
			lastSettings.shift();
		}

		if (currentAction) {
			currentAction = "Undo " + currentAction;
		} else {
			currentAction = "Undo last change";
		}
		setSetting("undoAction", currentAction);
		$("#swlUndoButton")
			.attr("disabled", "")
			.attr("title", currentAction);
	};

	// restore previous settings
	var undo = function() {
		if (lastSettings.length > 0) {

			var currentControls = settings.controls;
			settings = lastSettings.pop();
			settings.controls = currentControls; // controls aren't subject to undo

			// only rebuild menus when needed because it takes several seconds
			if (getSetting("rebuildCategoriesOnUndo") == "rebuild") {
				rebuildCategoryMenus(); // also updates display and local storage
			} else {
				writeLocalStorage();
				applySettings();
			}

			var lastAction = getSetting("undoAction");
			if (!lastAction) {
				lastAction = "";
			}
			$("#swlUndoButton").attr("title", lastAction);

			if (lastSettings.length == 0) {
				$("#swlUndoButton")
					.attr("disabled", "disabled")
					.attr("title", "Nothing to undo");
			}
		}
	};

	// for use after a change to the category settings
	var rebuildCategoryMenus = function() {

		// rebuild existing category menus
		$('.swlCategoryMenu').each(function() {
			var $newMenu = $constructCategoryMenu($(this).attr('withMeta'));
			$newMenu.attr("onChange", $(this).attr("onChange")); // retain old menu action
			this.parentNode.replaceChild($newMenu.get(0), this);
		});

		// update menu selections and save settings
		changeDisplayedCategory(
			selectCategoryMenu($("#swlSettingsPanelCategorySelector"), getSetting("controls", "displayedCategory")));

		initDisplayControls();
	};

	// read from local storage to current in-work settings during initialization
	var readLocalStorage = function() {
		if (storage) {

			var storedString = storage.getItem(storageKey);
			if (storedString) {

				try {
					settings = JSON.parse(storedString);
				} catch (e) {
					alert("Smart watchlist: error loading stored settings!");
					settings = {};
				}
			}

			// delete all obsolete local storage keys from prior versions and bugs
			// this can eventually go away
			var obsoleteKeys = [
				"undefinedmarkedUsers",
				"undefinedmarkedPages",
				"undefinedpatrolledRevs",
				"undefinedhiddenRevs",
				"undefinedGUI",
				"SmartWatchlist.flaggedPages",
				"SmartWatchlist.flaggedUsers",
				"SmartWatchlist.hiddenPages",
				"SmartWatchlist.hiddenUsers",
				"SmartWatchlist.markedUsers",
				"SmartWatchlist.markedPages",
				"SmartWatchlist.patrolledRevs",
				"SmartWatchlist.hiddenRevs",
				"SmartWatchlist.GUI",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".markedUsers",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".markedPages",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".patrolledRevs",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".userFlag",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".pageCategory",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".pageFlag",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".patrolledRevision",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".hiddenRevs",
				"SmartWatchlist." + mw.config.get("wgUserName") + ".GUI",
				"length"
			];
			for (var i in obsoleteKeys) {
				if (typeof(storage.getItem(obsoleteKeys[i])) !== "undefined") {
					storage.removeItem(obsoleteKeys[i]);
				}
			}
		}
	};

	// update local storage to current in-work settings
	var writeLocalStorage = function() {
		if (storage) {
			var storeString = JSON.stringify(settings);
			var size = storeString.length;
			if (size > maxSettingsSize) {
				storeString = "";
				alert("Smart watchlist: new settings are too large to be saved (" + size + " bytes)!")
				return;
			}

			var lastSaveString = storage.getItem(storageKey);

			try {
				storage.setItem(storageKey, storeString);
			} catch (e) {
				storeString = "";
				alert("Smart watchlist: error saving new settings!");

				// revert to previously saved settings that seemed to work
				storage.setItem(storageKey, lastSaveString);
			}
			maxUndo = Math.floor(maxSettingsSize / size) + 2;
		}
	};

	// erase all saved settings
	var clearSettings = function() {
		snapshotSettings("clear settings", "rebuild");
		var currentControls = settings.controls;
		settings = {};
		settings.controls = currentControls; // controls aren't subject to clearing
		initSettings();
		rebuildCategoryMenus(); // also updates display and local storage
	};

	// lookup a setting path passed as a series of arguments
	// returns undefined if no setting exists
	var getSetting = function() {
		var obj = settings;
		for (var index in arguments) {
			if (typeof(obj) !== "object") {
				return undefined; // part of path is missing
			}
			obj = obj[arguments[index]];
		}
		return obj;
	};

	// set the value of a setting path passed as a series of argument strings
	// creates intermediate objects as needed
	// number arguments reference arrays and string arguments reference associative array properties
	// the last argument is the value to be set (can be any type)
	var setSetting = function() {
		if (arguments.length < 2) {
			throw "setSetting: insufficient arguments";
		}
		var obj = settings;
		for (var index = 0; index < arguments.length - 2; index++) {
			var nextObj = obj[arguments[index]];
			if (typeof(nextObj) !== "object") {
				if (typeof(arguments[index + 1]) === "number") {
					nextObj = obj[arguments[index]] = [];
				} else {
					nextObj = obj[arguments[index]] = {};
				}
			}
			obj = nextObj;
		}
		obj[arguments[arguments.length - 2]] = arguments[arguments.length - 1];
	};

	// delete a setting path passed as a series of argument strings if the entire path exists
	var deleteSetting = function() {
		if (arguments.length < 1) {
			throw "deleteSetting: insufficient arguments";
		}
		var obj = settings;
		for (var index = 0; index < arguments.length - 1; index++) {
			// check if we hit a snag and still have more arguments to go
			if (typeof(obj) !== "object") {
				return;
			}
			obj = obj[arguments[index]];
		}
		if (typeof(obj) === "object") {
			delete obj[arguments[index]];
		}
	};

	var initSettings = function() {

		// check if home domain already exists
		if (!getSetting("wikis", document.domain)) {
			setSetting("wikis", document.domain, "active", true);
			var wikiNumber = 0;
			var wikiList = getSetting("wikiList");
			if (wikiList) {
				wikiNumber = wikiList.length;
			}
			setSetting("wikiList", wikiNumber, {
				domain: document.domain,
				displayName: document.domain
			});
		}

		if (!settings.nextCategoryKey) {
			settings.nextCategoryKey = 1;
		}
	};

	// dialog windows
	var setupCategories = null;
	mw.loader.using(['jquery.ui.dialog', 'jquery.ui.sortable'], function() {

		setupCategories = function() {

			// construct a category name row for editing
			var addCategory = function(key, name) {
				$editTable.append(
					$('<tr />')
					.append(
						$('<td />').append($('<span />').addClass('ui-icon ui-icon-arrowthick-2-n-s'))
					)
					.append(
						$('<td />').append(
							$('<input />', {
								type: 'text',
								size: '20',
								categoryKey: key,
								value: name
							})
						)
					)
				);
			};

			// jQuery UI sortable() seems to only like <ul> top-level elements
			var $editTable = $('<ul />').sortable({
				axis: 'y'
			});

			for (var i in settings.userCategories) {
				addCategory(settings.userCategories[i].key,
					settings.userCategories[i].name);
			}
			if (!getSetting('userCategories', 0)) {
				addCategory(settings.nextCategoryKey++, ''); // pre-add first category if needed
			}

			var $interface = $('<div />')
				.css({
					'position': 'relative',
					'margin-top': '0.4em'
				})
				.append(
					$('<ul />')
					.append($('<li />', {
						text: "Renamed categories retain current pages."
					}))
					.append($('<li />', {
						text: "Dragging lines changes the order in category menus."
					}))
					.append($('<li />', {
						text: "To delete a category, blank its name."
					}))
					.append($('<li />', {
						text: "Pages in deleted categories revert to uncategorized."
					}))
				)
				.append($('<br />'))
				.append($editTable)
				.append($('<br />'))
				.dialog({
					width: 400,
					autoOpen: false,
					title: 'Custom category setup',
					modal: true,
					buttons: {
						'Save': function() {
							$(this).dialog('close');
							snapshotSettings('category setup', 'rebuild');

							// replace category names in saved settings
							deleteSetting('userCategories');
							var index = 0;
							$editTable.find('input').each(function() {

								var name = $.trim(this.value);
								if (name.length > 0) { // skip blank categories

									// convert category key back into a number
									var key = $(this).attr('categoryKey');
									if (typeof(key) === "string") {
										var intKey = parseInt(key);
										if (!isNaN(intKey)) {
											setSetting('userCategories', index++, {
												key: intKey,
												name: name
											});
										}
									}
								}
							});
							rebuildCategoryMenus();
						},
						'Add category': function() {
							addCategory(settings.nextCategoryKey++, '');
						},
						'Cancel': function() {
							$(this).dialog('close');
						}
					}
				});
			$interface.dialog('open');
		}
	});

	// activate only on the watchlist page
	if (mw.config.get("wgNamespaceNumber") == -1 && mw.config.get("wgTitle") == "Watchlist") {
		$(document).ready(initialize);
	};
})();
/* </pre> */