User:Magog the Ogre/ExpandedWatchlist.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:Magog the Ogre/ExpandedWatchlist. |
- Report page listing warnings and errors.
/***********************
* This is the source code version. To make an update, you will need to download Closure compiler on your machine. Sorry; the public web API is old and doesn't properly polyfill for IE11. Hopefully this will change soon.
Instructions:
* 1) Edit this file in ES6
* 2) Copy the contents of this file
* 3) Compress the file in Closure compiler. Use the options
--assume_function_wrapper
--language_in ECMASCRIPT8
--compilation_level SIMPLE
* 4) Copy the compiled code
* 5) Go to https://commons.wikimedia.org/w/index.php?title=MediaWiki:Gadget-ExpandedWatchlist.js&action=edit
* 6) Insert the compiled code at the appropriate marker
*
* If you'd like to add your own option, you can add the template below to your skin.js
mw.hook("gadget.expandedWatchlist.define").add(function () {
function MyFilter() {
};
MyFilter.prototype = $.extend({},
window.ExpandedWatchlist.ShowHideFilter.prototype,
{
get name() {
return "my-name";
}
toolFilter: function (all) {
return all.filter(function (_, element) {
//add logic here
});
}
}
);
window.ExpandWatchlist.addToolMessages({
"my-name": {
shortDesc: {
en: "My short description"
},
longDesc: {
en: "My long description"
}
}
});
window.ExpandedWatchlist.addFilters(MyFilter);
});
/* jshint ignore:start */
(async (window) => {
const $ = window.$;
const mw = window.mw;
function objectFromEntries(entries) {
return Object.assign({}, ...$.map(entries, ([key, val]) => ({[key]: val})));
}
/**
* Mediawiki takes WAY too long for the document ready to load; this will listen for
* the actual body being loaded before that.
* @return Promise
*/
async function onLoad() {
return new Promise(resolve => {
$(() => {
resolve();
});
var interval = window.setInterval(() => {
if ($("#footer")[0] && $("#mw-navigation")[0]) {
resolve();
window.clearInterval(interval);
}
}, 100);
});
}
class GlobalSingletonFactory {
constructor() {
this._registry = new Map();
this._factories = [];
}
get(_class) {
var singleton = this._registry.get(_class);
if (!singleton) {
this._factories.some((_factory) => {
singleton = _factory.get(_class);
return singleton;
});
this._registry.set(_class, singleton);
}
return singleton;
}
register(_factory) {
this._factories.unshift(_factory);
}
}
class DefaultSingletonFactory {
get(_class) {
return new _class();
}
}
class StorageFactory {
/**
* @return {window.Storage}
*/
get(_class) {
if (_class === window.Storage) {
const STORAGE_CHECK = "gadget-ew-check";
return [window.localStorage, window.sessionStorage].find((_storage) => {
try {
_storage.setItem(STORAGE_CHECK, 0);
_storage.removeItem(STORAGE_CHECK);
return true;
} catch (e) {
// empty
}
});
}
}
}
class Preferences {
constructor() {
const _storageKey = "gadget-ew";
var storage = factory.get(window.Storage);
var map = Object.assign({}, JSON.parse(storage.getItem(_storageKey)) || {});
this.get = (key) => {
return map[key];
};
this.set = (key, val) => {
map[key] = val;
storage.setItem(_storageKey, JSON.stringify(map));
};
}
}
class ExpandedWatchlistNotifier {
constructor() {
this._preferences = factory.get(Preferences);
this._message = factory.get(Messages).getGlobal("disableOptions");
}
notify(badOptions) {
if (badOptions[0] && !this.getNotified()) {
this.doNotify(badOptions);
}
}
doNotify(badOptions) {
$("<div></div>")
.html(this.getMessage(badOptions))
.dialog({
appendTo: "body",
modal: true,
buttons: {
[factory.get(Messages).getGlobal("gotIt")]: function () {
$(this).dialog( "close" );
}
},
close: () => {
this.setNotified();
}
});
}
getMessage(badOptions) {
var optionList = badOptions.map((_, input) =>
$.trim($("label[for=" + $(input).attr("id") + "]").text())
).get().join(", ");
return this._message.replace(/\$1\b/, optionList);
}
getNotified() {
return this._preferences.get("notified");
}
setNotified() {
this._preferences.set("notified", true);
}
}
class Messages {
constructor() {
this._mylang = mw.config.get("wgUserLanguage");
this._defaultLang = "en";
this.global = {
show: {
en: "Show"
},
only: {
en: "Show only"
},
hide: {
en: "Hide"
},
disabled: {
en: "disabled"
},
disableOptions: {
en: "Expanded watchlist will only filter entries returned in your standard watchlist. " +
"In order to make the most out of expanded watchlist, you may wish to <strong>edit " +
"your preferences</strong> to only hide options you never want to see. <br/><br/>" +
"The following options are currently disabled: $1."
},
gotIt: {
en: "Got it"
},
expandWatchlist: {
en: "Expanded watchlist"
},
standardWatchlist: {
en: "Standard watchlist"
},
rememberSettings: {
en: "Remember settings"
},
reset: {
en: "Clear state"
},
resetSettings: {
en: "Reset settings"
},
save: {
en: "Save state"
},
"preferences-saved": {
en: "Preferences saved"
},
"preferences-cleared": {
en: "Preferences cleared"
}
};
this.tools = {
"ew-bot": {
shortDesc: {
en: "Bots"
},
longDesc: {
en: "Bot edits"
}
},
"ew-log": {
shortDesc: {
en: "Logs"
},
longDesc: {
en: "All log entries"
}
},
"ew-uploads": {
shortDesc: {
en: "Upload log"
},
longDesc: {
en: "Upload log"
}
},
"ew-deletion": {
shortDesc: {
en: "Deletion log"
},
longDesc: {
en: "Deletion log"
}
},
"ew-new": {
shortDesc: {
en: "New pages"
},
longDesc: {
en: "New pages"
}
},
"ew-patrolled": {
shortDesc: {
en: "Patrolled pages"
},
longDesc: {
en: "Patrolled pages"
}
},
"ew-data": {
shortDesc: {
en: "Wikidata"
},
longDesc: {
en: "Wikidata"
}
},
"ew-minor": {
shortDesc: {
en: "Minor edits"
},
longDesc: {
en: "Minor edits"
}
},
"ew-anon": {
shortDesc: {
en: "Anonymous users"
},
longDesc: {
en: "Anonymous users"
}
},
"ew-cat": {
shortDesc: {
en: "Page categorization"
},
longDesc: {
en: "Page categorization"
}
},
"ew-mine": {
shortDesc: {
en: "My edits"
},
longDesc: {
en: "My edits"
}
},
"ew-experienced": {
shortDesc: {
en: "Experienced users"
},
longDesc: {
en: "Experienced accounts"
}
},
"ew-learner": {
shortDesc: {
en: "Learners"
},
longDesc: {
en: "Learner accounts"
}
},
"ew-newcomer": {
shortDesc: {
en: "Newcomers"
},
longDesc: {
en: "Newcomer accounts"
}
},
"ew-deleted": {
shortDesc: {
en: "Deleted pages"
},
longDesc: {
en: "Target pages is deleted."
}
}
};
}
_getMessage(parent) {
return parent[this._myLang] || parent[this._defaultLang];
}
getGlobal(key) {
try {
return this._getMessage(this.global[key]);
} catch (e) {
mw.log.error(`Global message not found: ${key}`);
return "";
}
}
getTool(toolname, key) {
try {
return this._getMessage(this.tools[toolname][key]);
} catch (e) {
mw.log.error(`Message not found: ${toolname}.${key}`);
return "";
}
}
addToolMessages(messages) {
$.extend(true, this.tools, messages);
}
setMylang(lang) {
this._mylang = lang;
}
}
class ExpandedWatchlist {
notifyIfBadOptions(badOptions) {
factory.get(ExpandedWatchlistNotifier).notify(badOptions);
}
constructor() {
this.AbstractExpandedWatchlistFilter = AbstractExpandedWatchlistFilter;
this.ExistingFilter = ExistingFilter;
this.ShowHideFilter = ShowHideFilter;
this._messages = factory.get(Messages);
this._filters = [
// //////////////////
// logs
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-log";
}
getToolFilter() {
return ".mw-changeslist-src-mw-log";
}
},
// //////////////////
// uploads
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-uploads";
}
getToolFilter() {
return ".mw-changeslist-log-upload";
}
},
// //////////////////
// deletions
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-deletion";
}
getToolFilter() {
return ".mw-changeslist-log-delete";
}
},
// //////////////////
// minor edits
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-minor";
}
getToolFilter() {
return ".mw-changeslist-minor";
}
getOriginalElementIds() {
return ["hideminor"];
}
},
// //////////////////
// new pages
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-new";
}
getToolFilter() {
return ".mw-changeslist-src-mw-new";
}
},
// //////////////////
// patrolled pages
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-patrolled";
}
getToolFilter() {
return ".mw-changeslist-patrolled";
}
getOriginalElementIds() {
return ["hidepatrolled"];
}
},
// //////////////////
// Wikidata
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-data";
}
getToolFilter() {
return ".mw-changeslist-src-wb";
}
getOriginalElementIds() {
return ["hideWikibase"];
}
},
// //////////////////
// Page categorization
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-cat";
}
getToolFilter() {
return ".mw-changeslist-src-mw-categorize";
}
getOriginalElementIds() {
return ["hidecategorization"];
}
},
// //////////////////
// Deleted pages
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-deleted";
}
doFilter(all) {
return all.children(".new:not(.mw-userlink)").closest(all);
}
},
// //////////////////
// My edits
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-mine";
}
getToolFilter() {
return ".mw-changeslist-self";
}
getOriginalElementIds() {
return ["hidemyself"];
}
},
// //////////////////
// Experienced
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-experienced";
}
getToolFilter() {
return ".mw-changeslist-user-experienced";
}
getOriginalElementIds() {
return ["hideliu"];
}
},
// //////////////////
// Learners
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-learner";
}
getToolFilter() {
return ".mw-changeslist-user-learner";
}
getOriginalElementIds() {
return ["hideliu"];
}
},
// //////////////////
// Learners
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-newcomer";
}
getToolFilter() {
return ".mw-changeslist-user-newcomer";
}
getOriginalElementIds() {
return ["hideliu"];
}
},
// //////////////////
// Anonymous users
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-anon";
}
getToolFilter() {
return ".mw-changeslist-anon";
}
getOriginalElementIds() {
return ["hideanons", "hideliu"];
}
getDisabledState() {
if ($("#hideanons:checked")[0]) {
return "hide";
}
if ($("#hideliu:checked")[0]) {
return "only";
}
}
},
// //////////////////
// bot edits
// //////////////////
class extends ShowHideFilter {
getName() {
return "ew-bot";
}
getToolFilter() {
return ".mw-changeslist-bot";
}
getOriginalElementIds() {
return ["hidebots"];
}
},
// //////////////////
// namespace
// //////////////////
class extends ExistingFilter {
getName() {
return "ew-namespace";
}
getChangeElements() {
return $("#namespace,#nsinvert,#nsassociated");
}
filter(all) {
var val = $("#namespace").val();
if (!val) {
return all;
}
var inverted = $("#nsinvert").is(":checked");
var regexes = [new RegExp(`(^|\\s)(mw\\-changeslist\\-ns|watchlist\\-)${val}\\-`)];
if ($("#nsassociated").is(":checked")) {
val = (val % 2 ? -1 : 1) + +val;
regexes.push(new RegExp(`(^|\\s)(mw\\-changeslist\\-ns|watchlist\\-)${val}\\-`));
}
var filtered = all.filter((_, element) =>
regexes.some((regex) => regex.test($(element).attr("class")))
);
if (inverted) {
filtered = all.not(filtered);
}
return filtered;
}
}
].map((Filter) => {
var filter = new Filter();
filter.setNotify(this.filterAll.bind(this));
return filter;
});
}
getFilters() {
return this._filters;
}
getAllRows() {
return $(".mw-changeslist > ul > li");
}
filterNone() {
this.getAllRows().show();
}
filterAll() {
var all = this.getAllRows();
var showing = all;
this.getFilters().forEach((filter) => {
showing = filter.filter(showing);
});
showing.show();
all.not(showing).hide();
}
load() {
var submitButton = $("#mw-watchlist-options :submit");
var saveButton = $("<input type=\"button\" />").val(
factory.get(Messages).getGlobal("save"))
.click(() => {
this.saveState();
})[0];
var resetButton = $("<input type=\"button\" />").val(
factory.get(Messages).getGlobal("reset"))
.click(() => {
this.reset();
})[0];
this._originalElements = $($.map(this.getFilters(), filter => filter.getOriginalElements()));
this._toggleElements = $($.map(this.getFilters(),
filter => filter.loadElements())).prepend("<br/>").
add("<br/>").add([saveButton, resetButton]);
this._toggleButton = $("<input type=\"button\" />")
.click(this.toggle.bind(this)).insertAfter(submitButton).after(this._toggleElements);
this._untoggleElements = this._originalElements.closest(".mw-input-with-label").
add(submitButton);
this.loadState();
this.toggle();
}
toggle() {
this._active = !this._active;
if (this._active) {
this.notifyIfBadOptions(this._originalElements.filter(":checked"));
this.filterAll();
} else {
this.getAllRows().show();
}
this._untoggleElements.toggle(!this._active);
this._toggleElements.toggle(this._active);
this._toggleButton.val(factory.get(Messages).getGlobal(this._active ?
"standardWatchlist" : "expandWatchlist"));
}
addFilters(filters) {
this._filters = this._filters.concat(filters);
}
reset() {
var preferences = factory.get(Preferences);
var messages = factory.get(Messages);
preferences.set("expanded", false);
preferences.set("state", "");
mw.notify(messages.getGlobal("preferences-cleared"));
}
saveState() {
var preferences = factory.get(Preferences);
var messages = factory.get(Messages);
preferences.set("expanded", true);
preferences.set("state",
JSON.stringify(objectFromEntries(this._filters.map(filter =>
[filter.getName(), filter.serialize()]))));
mw.notify(messages.getGlobal("preferences-saved"));
}
loadState() {
var preferences = factory.get(Preferences);
this._active = !preferences.get("expanded");
var state = factory.get(Preferences).get("state");
if (state) {
let stateObject = JSON.parse(state);
this._filters.forEach(filter => {
filter.unserialize(stateObject[filter.getName()]);
});
}
}
}
class AbstractExpandedWatchlistFilter {
// abstract function: filter
// abstract function: loadElements
// abstract function: serialize
// abstract function: unserialize
// abstract function: getName
getMyMessage(key) {
return factory.get(Messages).getTool(this.getName(), key);
}
/**
* The original element on the watchlist that will be hidden when this
* one is shown.
*
* @return {jQuery}
*/
getOriginalElementIds() {
return [];
}
getOriginalElements() {
return this.getOriginalElementIds().map(id => document.getElementById(id)).
filter(element => element);
}
setNotify(_notify) {
this._notify = _notify;
}
getNotify() {
return this._notify;
}
}
class ExistingFilter extends AbstractExpandedWatchlistFilter {
loadElements() {
this.getChangeElements().change(this.getNotify());
return [];
}
serialize() {
return JSON.stringify(objectFromEntries(this.getChangeElements().get().map(element =>
[$(element).attr("id"), $(element).val()])));
}
unserialize(fromString) {
var unserialized = JSON.parse(fromString);
Object.keys(unserialized).forEach(id => {
$(document.getElementById(id)).val(unserialized[id]);
});
}
// abstract function: getChangeElements
}
class ShowHideFilter extends AbstractExpandedWatchlistFilter {
filter(all) {
var state = $(document.getElementsByName(this.getName())).filter(":checked").val();
if (state !== "show") {
var filtered = this.doFilter(all);
if (state === "hide") {
filtered = all.not(filtered);
}
all = filtered;
}
return all;
}
doFilter(all) {
return all.filter(this.getToolFilter());
}
loadElements() {
var disabledState = this.getDisabledState();
var longDesc = this.getMyMessage("longDesc");
var elements = $.map(["show", "hide", "only"], (fn) => {
var id = `${this.getName()}-${fn}`;
var checked;
if (disabledState) {
checked = disabledState === fn;
} else {
checked = fn === "show";
}
var radio = $("<input type='radio'/>").
attr({
id: id,
value: fn,
name: this.getName(),
title: longDesc,
disabled: !!disabledState
}).prop("checked", checked).change(this.getNotify());
var label = $("<label/>").attr({
for: id,
title: longDesc
}).html(factory.get(Messages).getGlobal(fn));
return [radio[0], label[0]];
});
var shortDesc = this.getMyMessage("shortDesc");
if (disabledState) {
shortDesc += $("<span style=\"font-size: 90%\"></span>").
html("(" + factory.get(Messages).getGlobal("disabled") + ")")[0].outerHTML;
}
elements.unshift($("<span></span>").html(shortDesc).attr("title", longDesc)[0]);
if (disabledState) {
$(elements).css("color", "#848484");
}
this._radios = $(elements).filter("input");
return $("<div style=\"line-height: 0.5em;\"></div>").append(elements)[0];
}
getDisabledState() {
return $(this.getOriginalElements()).is(":checked") && "hide";
}
serialize() {
return this._radios.filter(":checked").val() || "";
}
unserialize(fromString) {
this._radios.prop("checked", (index) => this._radios.eq(index).val() === fromString);
}
// abstract function: getToolFilter()
}
if (mw.config.get("wgPageName") === "Special:Watchlist") {
var factory = new GlobalSingletonFactory();
factory.register(new DefaultSingletonFactory());
factory.register(new StorageFactory());
let expandedWatchlist = window.expandedWatchlist = factory.get(ExpandedWatchlist);
mw.hook("gadget.expandedWatchlist.define").fire();
await onLoad();
expandedWatchlist.load();
}
})(window);
/* jshint ignore:end */