User:UncleDouggie/smart watchlist.js

/** Smart watchlist
 * Provides ability to selectively hide and/or highlight changes in a user's watchlist display.
 * Author: User:UncleDouggie
 * Author: User:UncleDouggie

// 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
 * 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: ...
 * }
 * }

// 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);	}; } ) ;