Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: hold down the Ctrl key and click the Refresh or Reload button. Firefox: hold down the Shift key while clicking Reload (or press Ctrl-Shift-R). Google Chrome and Safari users can just click the Reload button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/** 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() { // 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> </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(" ") .append( $("<input />", { type: "button", onClick: "javascript:SmartWatchlist.setupCategories();", title: "Create, change and delete custom category names", value: "Setup categories" } ) ) .append(" ") .append( $("<input />", { type: "button", id: "swlUndoButton", onClick: "javascript:SmartWatchlist.undo();", title: "Nothing to undo", disabled: "disabled", value: "Undo" } ) ) .append( "<p />" ) .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 = $( "<div />" ).attr("align", "right") .append( "Sort order: " ); for (var i = 0; i < maxSortLevels; i++) { $sortPanel .append( $constructSortMenu().attr("selectedIndex", i) ) .append( "<br />" ); if (i == 0) { $sortPanel.append( "(not yet) " ); } } // 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); }; } ) ();