User:Averruncus/monobook.js

/** Smart watchlist // Extend jQuery to add a simple color picker optimized for our use ( function {	// 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 = $( " " )		.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) { $(" ").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 // 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  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 = $("", {						href: "javascript:SmartWatchlist.hideRev('" + pageID + "', '" + revID + "');",						title: "Hide this change",						text: "hide change"					}); $td.append( $( " " )						.addClass( "swlRevisionButton" )						.append( " [" ).append( $revLink ).append( "]" )					); // add the patrol prior changes link var $patrolLink = $("", {						href: "javascript:SmartWatchlist.patrolRev('" + pageID + "', '" + revID + "');",						title: "Hide previous changes",						text: "patrol"					}); $td.append( $( " " )						.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  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 = $("", {					href: "javascript:SmartWatchlist.hideUser('" + user + "');",					title: "Hide changes by " + user + " on all pages",					text: "hide user"				}); $td.append( $( " " )					.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( $( " ", {						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 = $( " " ).attr("valign", "top")			.append( $( " ", {					type: "checkbox",					"class": "swlOptionCheckbox",					controlledClass: "swlRevisionButton",					controlsProperty: "showRevisionButtons",					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"				} ) )			.append("Enable hide/patrol change buttons")			.append( " " )			.append( $( " ", {					type: "checkbox",					"class": "swlOptionCheckbox",					controlledClass: "swlHideUserButton",					controlsProperty: "showUserButtons",					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"				} ) )			.append("Enable hide user buttons")			.append( " " )			.append( $( " ", {					type: "checkbox",					"class": "swlOptionCheckbox swlColorPickerControl",					controlledClass: "swlUserLink",					controlsProperty: "showUserColorPickers",					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"				} ) )			.append("Assign user highlight colors")			.append( " " )			.append( $( " ", {					type: "checkbox",					"class": "swlOptionCheckbox swlColorPickerControl",					controlledClass: "swlPageTitleLink",					controlsProperty: "showPageColorPickers",					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"				} ) )			.append("Assign page highlight colors")			.append( " " )			.append( $( " ", {					type: "checkbox",					"class": "swlOptionCheckbox",					controlledClass: "swlPageCategoryMenu",					controlsProperty: "showPageCategoryButtons",					onClick: "javascript:SmartWatchlist.processOptionCheckbox(this);"				} ) )			.append("Assign page categories");		// construct panel column 2		var $column2 = $( " " )			.attr("style", "padding-left: 25pt;")			.append( $( " " ).attr("align", "center") .append(					$(" ", { type: "button", onClick: "javascript:SmartWatchlist.clearSettings;", title: "Reset all page and user settings and remove all custom categories", value: "Clear settings" } ) 				)				.append(" ") .append(					$(" ", { type: "button", onClick: "javascript:SmartWatchlist.setupCategories;", title: "Create, change and delete custom category names", value: "Setup categories" } )				)				.append(" ") .append(					$(" ", { type: "button", id: "swlUndoButton", onClick: "javascript:SmartWatchlist.undo;", title: "Nothing to undo", disabled: "disabled", value: "Undo" } ) 				)				.append( "" ) .append( "Display pages in: " ) .append( 					$constructCategoryMenu( "meta" )						// no attributes other than onChange allowed so the menu can be rebuild in setupCategories!						.attr( "onChange", "javascript:SmartWatchlist.changeDisplayedCategory(value);" )				) );		$sortPanel = $( " " ).attr("align", "right")			.append( "Sort order: " );		for (var i = 0; i < maxSortLevels; i++) {			$sortPanel			.append( $constructSortMenu.attr("selectedIndex", i) )			.append( " " );			if (i == 0) {				$sortPanel.append( "(not yet) " );			}		}		// construct panel column 3		var $column3 = $( " " )			.attr("style", "padding-left: 25pt;")			.append( $sortPanel );		// construct main settings panel		$("#mw-watchlist-options")			.after( $( " ", {					id: "SmartWatchlistOptions"				} ) .append( 					$( " ", { text: "Smart watchlist settings" } ) 				)				.append( 					$( " " )					.append( $( " " )						.append( $column1 ) .append( 							$( " ", { valign: "top" } )							.append( $column2 )						) .append( 							$( " ", { valign: "top" } )							.append( $column3 )						) )				)			);		if ( !storage ) {			$("#SmartWatchlistOptions")			.append( $( "", {					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 = 			$( " ", { "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 elements for (var i in categories) { $selector.append( $( " ", categories[i] ) ); }		return $selector; };	// construct a page category menu var $constructSortMenu = function { var $selector = $( " ", {				"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 elements for (var i in sortCriteria) { $selector.append( $( " ", 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'], function {		setupCategories = function {			// construct a category name row for editing			var addCategory = function ( key, name ) {				$editTable.append( $( ' ' )					.append( 						$( ' ' ).append( $( ' ' ).addClass( 'ui-icon ui-icon-arrowthick-2-n-s' ) )					) .append(						$( ' ' ).append( $( ' ', {								type: 'text',								size: '20',								categoryKey: key,								value: name							} ) )					)				);			};			// jQuery UI sortable seems to only like  top-level elements			var $editTable = $( ' ' ).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 = $(' ')				.css( { 'position': 'relative', 'margin-top': '0.4em' } )				.append( $( ' ')					.append( $( ' ', { text: "Renamed categories retain current pages." } ) )					.append( $( ' ', { text: "Dragging lines changes the order in category menus." } ) )					.append( $( ' ', { text: "To delete a category, blank its name." } ) )					.append( $( ' ', { text: "Pages in deleted categories revert to uncategorized." } ) )				)				.append( $( ' ' ) )				.append( $editTable )				.append( $( ' ' ) )				.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); }; } ) ;
 * Provides ability to selectively hide and/or highlight changes in a user's watchlist display.
 * Author: User:UncleDouggie
 * Author: User:UncleDouggie
 * 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: ...
 * }
 * }
 * username2: ...
 * }
 * },
 * wiki domain 2: ...
 * }
 * }