User:UncleDouggie/smart watchlist.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:UncleDouggie/smart watchlist. |
/** 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'], 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);
};
} ) ();