MediaWiki:Gadget-watchlist.js: различия между версиями

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Содержимое удалено Содержимое добавлено
исправление неубирания всех звёздочек но кнопке, если исключили из СН что-то; изменение логики классов при ожидании результата действия с СН
рефакторинг классов, коррекция получения названия страницы из ссылки
Строка 72: Строка 72:
if (!allStarsShown) {
if (!allStarsShown) {
$content.find('.mw-title')
$content.find('.mw-title')
.each(function(i, link) {
.each(function (i, link) {
updateStar(getRow($(this)));
updateStar(getRow($(this)));
});
});
Строка 81: Строка 81:
allStarsShown = true;
allStarsShown = true;
} else { // otherwise remove
} else { // otherwise remove
$content
$content.find('.gadgetWatchlist-unwatchLink').remove();
.find('.gadgetWatchlist-unwatchWatchLink')
.not('.gadgetWatchlist-icon-unwatched')
.remove();
document.cookie = 'wlunw=0; expires=' + (new Date()).toGMTString() + '; path=/';
document.cookie = 'wlunw=0; expires=' + (new Date()).toGMTString() + '; path=/';
if (!hoverOnTitlesDone) {
if (!hoverOnTitlesDone) {
Строка 104: Строка 101:
// create sorting keys
// create sorting keys
var key;
var key;
$rows.each(function(i) {
$rows.each(function (i) {
// use built-in class: either li.watchlist-5-<title> or
// use built-in class: either li.watchlist-5-<title> or
// table.mw-changeslist-ns100-<title> in enhanced recent changes
// table.mw-changeslist-ns100-<title> in enhanced recent changes
Строка 117: Строка 114:
});
});
// sort array and then HTML
// sort array and then HTML
$rows.sort(function(a, b) {
$rows.sort(function (a, b) {
return a.skey > b.skey ? 1 : (a.skey < b.skey ? -1 : 0);
return a.skey > b.skey ? 1 : (a.skey < b.skey ? -1 : 0);
});
});
Строка 191: Строка 188:
$row.data('leaveAssigned', true);
$row.data('leaveAssigned', true);
$row.mouseleave(function (e) {
$row.mouseleave(function (e) {
var $unwatchWatchLink = $(this).find('.gadgetWatchlist-unwatchWatchLink');
var $unwatchLink = $(this).find('.gadgetWatchlist-unwatchLink');
if ($unwatchWatchLink.length &&
if ($unwatchLink.length &&
!($unwatchLink.hasClass('gadgetWatchlist-failure') ||
$unwatchWatchLink.attr('href').includes('&action=unwatch') &&
!($unwatchWatchLink.hasClass('gadgetWatchlist-unwatchWatchLink-failure') ||
$unwatchLink.hasClass('gadgetWatchlist-waiting')
$unwatchWatchLink.hasClass('gadgetWatchlist-icon-waiting')
) &&
) &&
!allStarsShown
!allStarsShown
) {
) {
$unwatchWatchLink.remove();
$unwatchLink.remove();
}
}
});
});
Строка 205: Строка 201:
function updateStar($row) {
function updateStar($row) {
var action = $row.hasClass('gadgetWatchlist-unwatchedRow') ? 'watch' : 'unwatch';
var icon, prevIcon, action;
if ($row.hasClass('gadgetWatchlist-unwatchedRow')) {
var $changeWatchStateLink = $row.find('.gadgetWatchlist-unwatchLink, .gadgetWatchlist-watchLink');
if (!$changeWatchStateLink.length) { // create
icon = 'unwatched';
prevIcon = 'watched';
$changeWatchStateLink = $('<a>')
action = 'watch';
} else {
icon = 'watched';
prevIcon = 'unwatched';
action = 'unwatch';
}
var $unwatchWatchLink = $row.find('.gadgetWatchlist-unwatchWatchLink');
if (!$unwatchWatchLink.length) { // create
$unwatchWatchLink = $('<a>')
.attr('href', $row
.attr('href', $row
.find('.mw-changeslist-title')
.find('.mw-changeslist-title')
Строка 223: Строка 210:
.replace(/\/wiki\//, '/ruwiki/w/index.php?title=') +
.replace(/\/wiki\//, '/ruwiki/w/index.php?title=') +
'&action=' + action)
'&action=' + action)
.addClass('gadgetWatchlist-unwatchWatchLink gadgetWatchlist-icon gadgetWatchlist-icon-' + icon)
.addClass('gadgetWatchlist-' + action + 'Link gadgetWatchlist-icon')
.click(ajaxUnwatchWatch)
.click(changeWatchState)
.insertBefore($row.find('.mw-changeslist-title'));
.insertBefore($row.find('.mw-changeslist-title'));
} else { // update
} else { // update
$unwatchWatchLink
$changeWatchStateLink
.attr('href', $unwatchWatchLink
.attr('href', $changeWatchStateLink
.attr('href')
.attr('href')
.replace(/&action=\w+/, '&action=' + action))
.replace(/&action=\w+/, '&action=' + action))
.removeClass('gadgetWatchlist-icon-' + prevIcon)
.removeClass('gadgetWatchlist-' + (action === 'unwatch' ? 'watch' : 'unwatch') + 'Link')
.addClass('gadgetWatchlist-icon-' + icon);
.addClass('gadgetWatchlist-' + action + 'Link');
}
}
$unwatchWatchLink.attr('title', mw.messages.get(action) || strings[action]);
$changeWatchStateLink.attr('title', mw.messages.get(action) || strings[action]);
}
}
Строка 241: Строка 228:
}
}
function ajaxUnwatchWatch(e) {
function changeWatchState(e) {
var $unwatchWatchLink = $(this), errorMsg = '';
var $changeWatchStateLink = $(this), errorMsg = '';
var req = {
var req = {
token: mw.user.tokens.get('watchToken'),
token: mw.user.tokens.get('watchToken'),
title: getLinkTitle($unwatchWatchLink)
title: getLinkTitle($changeWatchStateLink)
};
};
var isWatched = false;
var action;
if ($unwatchWatchLink.attr('href').includes('&action=unwatch')) {
if ($changeWatchStateLink.attr('href').includes('&action=unwatch')) {
isWatched = true;
req.unwatch = '';
req.unwatch = '';
action = 'unwatch';
} else {
action = 'watch';
}
}
$changeWatchStateLink
$unwatchWatchLink
.removeClass('gadgetWatchlist-unwatchWatchLink-failure gadgetWatchlist-icon-' +
.removeClass('gadgetWatchlist-failure')
.addClass('gadgetWatchlist-waiting');
(isWatched ? 'watched' : 'unwatched'))
.addClass('gadgetWatchlist-icon-waiting');
$.ajax({
$.ajax({
type: 'POST',
type: 'POST',
Строка 262: Строка 250:
data: req,
data: req,
timeout: 5000,
timeout: 5000,
success: function(resp) {
success: function (resp) {
if (resp.error) {
if (resp.error) {
errorMsg = resp.error.info;
errorMsg = resp.error.info;
Строка 268: Строка 256:
errorMsg = 'empty response';
errorMsg = 'empty response';
} else if (typeof resp.watch.unwatched === 'string') {
} else if (typeof resp.watch.unwatched === 'string') {
unwatchSuccess(req.title, true);
changeWatchStateSuccess(req.title, true);
} else if (typeof resp.watch.watched === 'string') {
} else if (typeof resp.watch.watched === 'string') {
unwatchSuccess(req.title, false);
changeWatchStateSuccess(req.title, false);
} else {
} else {
errorMsg = 'unrecognized response';
errorMsg = 'unrecognized response';
}
}
},
},
error: function(xhr, status, error) {
error: function (xhr, status, error) {
errorMsg = status + ':' + error;
errorMsg = status + ':' + error;
},
},
complete: function () { // update unwatch link
complete: function () { // update unwatch link
$unwatchWatchLink.removeClass('gadgetWatchlist-icon-waiting');
$changeWatchStateLink.removeClass('gadgetWatchlist-waiting');
if (errorMsg) {
if (errorMsg) {
$unwatchWatchLink
$changeWatchStateLink
.attr('title', 'API error: ' + errorMsg)
.attr('title', 'API error: ' + errorMsg)
.addClass('gadgetWatchlist-unwatchWatchLink-failure')
.addClass('gadgetWatchlist-failure gadgetWatchlist-' + action + 'Link');
.addClass('gadgetWatchlist-icon-' + (isWatched ? 'watched' : 'unwatched'));
} else {
$unwatchWatchLink.removeClass('gadgetWatchlist-unwatchWatchLink-failure');
}
}
}
}
Строка 294: Строка 279:
}
}
function unwatchSuccess(name, isUnwatched) {
function changeWatchStateSuccess(name, isUnwatched) {
// find full name of associated talk page (or vice versa)
// find full name of associated talk page (or vice versa)
var ns = getTitleNamespace(name);
var ns = getTitleNamespace(name);
Строка 310: Строка 295:
if (title !== name && title !== name2) return;
if (title !== name && title !== name2) return;
var $row = getRow($(this));
var $row = getRow($(this));
$row.toggleClass('gadgetWatchlist-unwatchedRow', isUnwatched || false);
$row.toggleClass('gadgetWatchlist-unwatchedRow', isUnwatched);
updateStar($row);
if (!isUnwatched && !allStarsShown) {
if (!isUnwatched && !allStarsShown) {
$row.find('.gadgetWatchlist-unwatchWatchLink').remove();
$row.find('.gadgetWatchlist-watchLink').remove();
} else {
updateStar($row);
}
}
});
});
Строка 383: Строка 369:
}
}
function getLinkTitle($link) { // gets 'title=' part from a link
function getLinkTitle($link) { // gets title for unwatch/watch links & common page links
return $link.filter('.mw-changeslist-title').attr('title') ||
// Titles can be absent for .mw-changeslist-title elements because of Popups extension.
mw.util.getParamValue('title', $link.attr('href')).replace(/_/g, ' ');
var title = $link.filter('.mw-changeslist-title').attr('title');
if (!title) {
title = mw.util.getParamValue('title', $link.attr('href'));
title = title && title.replace(/_/g, ' ');
}
if (!title) {
title = $link.filter('.mw-changeslist-title').text();
}
return title;
}
}

Версия от 20:50, 12 апреля 2018

(function () {
	
var firstRun = true;

if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist') {
	// Polyfill
	if (!String.prototype.includes) {
		String.prototype.includes = function (search, start) {
			'use strict';
			if (typeof start !== 'number') {
				start = 0;
			}
			
			if (start + search.length > this.length) {
				return false;
			} else {
				return this.indexOf(search, start) !== -1;
			}
		};
	}
	
	var SECONDS_IN_A_DAY = 1000 * 60 * 60 * 24;
	
	var whenPageLoadedOrUpdated;
	var hideInterfaceCSS, rvOffCSS;
	var sortingDone, allStarsShown, hoverOnTitlesDone;
	
	// ruwiki strings
	var strings = {
		watch: 'Следить',
		unwatch: 'Не следить',
		sortTip: 'Сортировать изменения по пространствам имён',
		sortDone: 'Изменения уже отсортированы',
		unwatchTip: 'Добавить/убрать звёздочки для вычёркивания страниц из списка наблюдения',
		newOnly: 'Только новые',
		newOnlyTip: 'Изменения с момента загрузки этой страницы',
		expandAll: 'Показать/спрятать все свёрнутые правки',
		switchRevert: 'Спрятать/показать ссылки «откатить»',
		fullPage: 'Спрятать/показать элементы интерфейса'
	};
	
	mw.hook('wikipage.content').add(function ($content) {
		function main() {
			/* FUNCTIONS */
			
			function addLink(contents, tip, classes) {
				return $('<a>')
					.attr('href', 'javascript:')
					.attr('title', tip)
					.addClass(classes)
					.append(contents)
					.appendTo($linksIn);
			}
			
			function newEntriesOnly(e) {
				var url = window.location.href.split('#')[0];
				var days = (Number(new Date()) - whenPageLoadedOrUpdated) / (1000 * 3600 * 24);
				if (days < 0) {
					days = 0.01;  // negative might happen when adjusting local time
				}
				e.target.href = /[?&]days=/.test(url) ?
					url.replace(/([?&]days=)[^&]*/, '$1' + days) :
					url + ((!url.includes('?')) ? '?' : '&') + 'days=' + days;
				return true;
			}
			
			function showAllStars(e) {
				if (e) {
					e.preventDefault();
				}
				// cookie set = stars are on
				if (!allStarsShown) {
					$content.find('.mw-title')
						.each(function (i, link) {
							updateStar(getRow($(this)));
						});
					if (e) {
						var cookieDate = new Date($.now() + SECONDS_IN_A_DAY * 90).toGMTString();
						document.cookie = 'wlunw=1; expires=' + cookieDate + '; path=/';
					}
					allStarsShown = true;
				} else {  // otherwise remove
					$content.find('.gadgetWatchlist-unwatchLink').remove();
					document.cookie = 'wlunw=0; expires=' + (new Date()).toGMTString() + '; path=/';
					if (!hoverOnTitlesDone) {
						bindHoverOnTitles();
					}
					allStarsShown = false;
				}
			}
			
			function sortWatchlist(e) {
				e.preventDefault();
				if (sortingDone) {
					alert(strings.sortDone);
					return;
				}
				$content.find('h4').each(function () {  // sort all days separately
					var $container = $(this).next('div, ul');
					var $rows = $container.children('li, table');
					// create sorting keys
					var key;
					$rows.each(function (i) {
						// use built-in class: either li.watchlist-5-<title> or
						// table.mw-changeslist-ns100-<title> in enhanced recent changes
						key = /(\d+)-(\S+)/.exec(this.className) || ['', 0, ' '];  // logs might not have this class
						if (key[1] % 2) {
							key[1]--;  // sort talk page as if it was a base page
						}
						if (window.watchlistSortNamespaceOnly) {
							key[2] = zzz(i);  // keep timestamp order within each NS block
						}
						this.skey = zzz(key[1]) + ':' +  key[2];
					});
					// sort array and then HTML
					$rows.sort(function (a, b) {
						return a.skey > b.skey ? 1 : (a.skey < b.skey ? -1 : 0);
					});
					for (i = 0; i < $rows.length; i++) {
						$container.append($rows.eq(i));
					}
				});
				$('.mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator').remove();
				sortingDone = true;
			}
			
			function expandMultipleEdits(e) {
				e.preventDefault();
				var $collapsibles = $('.mw-changeslist .mw-collapsible:not(.mw-changeslist-legend)');
				var $collapsible;
				var collapsed = $collapsibles.hasClass('mw-collapsed');  // at lease one
				for (var i = 0; i < $collapsibles.length; i++) {
					$collapsible = $collapsibles.eq(i);
					if ($collapsible.hasClass('mw-collapsed') === collapsed) {
						$collapsible.find('.mw-enhancedchanges-arrow').click();
					}
				}
			}
			
			function switchRevert(e) {
				if (e) {
					e.preventDefault();
				}
				if (!rvOffCSS) {
					rvOffCSS = mw.util.addCSS('\
						.mw-rollback-link {\
							display: none;\
						}\
					');
				} else {
					rvOffCSS.disabled = !rvOffCSS.disabled;
				}
				if (rvOffCSS.disabled) {
					document.cookie = 'wlrvoff=0; expires=' + (new Date()).toGMTString() + '; path=/';
				} else if (e) {
					var cookieDate = (new Date($.now() + SECONDS_IN_A_DAY * 90)).toGMTString();
					document.cookie = 'wlrvoff=1; expires=' + cookieDate + '; path=/';
				}
			}
			
			function bindHoverOnTitles() {  // find all title links and assign hover event
				if (hoverOnTitlesDone) return;
				$content.find('.mw-title')
					.each(function () {
						getRow($(this))
							.find('.mw-changeslist-title')
							.hover(hoverOnTitle);
					});
				hoverOnTitlesDone = true;
			}
			
			function hoverOnTitle(e) {  // on hover: add "unwatch" star after 1 second
				var $link = $(this);
				if (e.type === 'mouseenter') {
					$link.data('unwatchTimeout', setTimeout(function () {
						showStarOnHover($link);
					}, 1000));
				} else {
					clearTimeout($link.data('unwatchTimeout'));
				}
			}
			
			function showStarOnHover($link) {
				var $row = getRow($link);
				updateStar($row);
				// attach mouseleave to remove the star
				if ($row.data('leaveAssigned')) return;
				$row.data('leaveAssigned', true);
				$row.mouseleave(function (e) {
					var $unwatchLink = $(this).find('.gadgetWatchlist-unwatchLink');
					if ($unwatchLink.length &&
						!($unwatchLink.hasClass('gadgetWatchlist-failure') ||
							$unwatchLink.hasClass('gadgetWatchlist-waiting')
						) &&
						!allStarsShown
					) {
						$unwatchLink.remove();
					}
				});
			}
			
			function updateStar($row) {
				var action = $row.hasClass('gadgetWatchlist-unwatchedRow') ? 'watch' : 'unwatch';
				var $changeWatchStateLink = $row.find('.gadgetWatchlist-unwatchLink, .gadgetWatchlist-watchLink');
				if (!$changeWatchStateLink.length) {  // create
					$changeWatchStateLink = $('<a>')
						.attr('href', $row
							.find('.mw-changeslist-title')
							.attr('href')
							.replace(/\/wiki\//, '/ruwiki/w/index.php?title=') +
							'&action=' + action)
						.addClass('gadgetWatchlist-' + action + 'Link gadgetWatchlist-icon')
						.click(changeWatchState)
						.insertBefore($row.find('.mw-changeslist-title'));
				} else {  // update
					$changeWatchStateLink
						.attr('href', $changeWatchStateLink
							.attr('href')
							.replace(/&action=\w+/, '&action=' + action))
						.removeClass('gadgetWatchlist-' + (action === 'unwatch' ? 'watch' : 'unwatch') + 'Link')
						.addClass('gadgetWatchlist-' + action + 'Link');
				}
				$changeWatchStateLink.attr('title', mw.messages.get(action) || strings[action]);
			}
			
			function getRow($el) {
				return $el.closest(isEnhanced ? 'tr' : 'li');
			}
			
			function changeWatchState(e) {
				var $changeWatchStateLink = $(this), errorMsg = '';
				var req = {
					token: mw.user.tokens.get('watchToken'),
					title: getLinkTitle($changeWatchStateLink)
				};
				var action;
				if ($changeWatchStateLink.attr('href').includes('&action=unwatch')) {
					req.unwatch = '';
					action = 'unwatch';
				} else {
					action = 'watch';
				}
				$changeWatchStateLink
					.removeClass('gadgetWatchlist-failure')
					.addClass('gadgetWatchlist-waiting');
				$.ajax({
					type: 'POST',
					dataType: 'json',
					url: mw.util.wikiScript('api') + '?action=watch&format=json',
					data: req,
					timeout: 5000,
					success: function (resp) {
						if (resp.error) {
							errorMsg = resp.error.info;
						} else if (!resp.watch) {
							errorMsg = 'empty response';
						} else if (typeof resp.watch.unwatched === 'string') {
							changeWatchStateSuccess(req.title, true);
						} else if (typeof resp.watch.watched === 'string') {
							changeWatchStateSuccess(req.title, false);
						} else {
							errorMsg = 'unrecognized response';
						}
					},
					error: function (xhr, status, error) {
						errorMsg = status + ':' + error;
					},
					complete: function () {  // update unwatch link
						$changeWatchStateLink.removeClass('gadgetWatchlist-waiting');
						if (errorMsg) {
							$changeWatchStateLink
								.attr('title', 'API error: ' + errorMsg)
								.addClass('gadgetWatchlist-failure gadgetWatchlist-' + action + 'Link');
						}
					}
				});
				e.preventDefault();
				return false;
			}
			
			function changeWatchStateSuccess(name, isUnwatched) {
				// find full name of associated talk page (or vice versa)
				var ns = getTitleNamespace(name);
				var name2 = name;
				if (ns > 0) {
					name2 = name2.replace(/^.+?:/, '');  // remove old prefix
				}
				ns = ns % 2 ? ns - 1 : ns + 1;  // switch to associated namespace
				if (ns > 0) {
					name2 = mw.config.get('wgFormattedNamespaces')[ns] + ':' + name2;  // add new prefix
				}
				// mark all rows that are either name or name2
				$content.find('.mw-changeslist-title').each(function () {
					var title = getLinkTitle($(this));
					if (title !== name && title !== name2) return;
					var $row = getRow($(this));
					$row.toggleClass('gadgetWatchlist-unwatchedRow', isUnwatched);
					if (!isUnwatched && !allStarsShown) {
						$row.find('.gadgetWatchlist-watchLink').remove();
					} else {
						updateStar($row);
					}
				});
			}
			
			function hideInterface(e) {
				if (e) {
					e.preventDefault();
				}
				
				var hideInterfaceCSSCode = '\
					#firstHeading,\
					div#siteNotice,\
					#contentSub,\
					fieldset#mw-watchlist-options,\
					div.mw-rc-label-legend,\
					#mw-fr-watchlist-pending-notice,\
					.mw-indicators,\
					.mw-rcfilters-ui-filterTagMultiselectWidget,\
					.mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable,\
					.mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton,\
					.mw-rcfilters-ui-watchlistTopSectionWidget-separator {\
						display: none;\
					}\
					\
					.client-js .mw-special-Watchlist .rcfilters-head.rcfilters-head {\
						min-height: auto;\
					}\
				';
				
				if (!hideInterfaceCSS) {
					// If the new filters are on, wait until the interface is initialized.
					if (mw.user.options.get('rcenhancedfilters')) {
						hideInterfaceCSS = mw.util.addCSS(hideInterfaceCSSCode);
						$('.mw-rcfilters-ui-markSeenButtonWidget')
							.wrap('<div>')
							.parent()
							.addClass('mw-rcfilters-ui-markSeenButtonWidget-container')
							.appendTo('.mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails');
					} else {
						hideInterfaceCSS = mw.util.addCSS(hideInterfaceCSSCode);
					}
				} else {
					hideInterfaceCSS.disabled = !hideInterfaceCSS.disabled;
					if (mw.user.options.get('rcenhancedfilters')) {
						$('.mw-rcfilters-ui-markSeenButtonWidget-container')
							.appendTo(hideInterfaceCSS.disabled ?
								'.mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable .mw-rcfilters-ui-cell:first-child' :
								'.mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails');
					}
				}
				
				if (e) {
					if (!hideInterfaceCSS.disabled) {
						var cookieDate = new Date($.now() + SECONDS_IN_A_DAY * 90).toGMTString();
						document.cookie = 'wlmax=1; expires=' + cookieDate + '; path=/';
					} else {
						document.cookie = 'wlmax=0; expires=' + (new Date()).toGMTString() + '; path=/';
					}
				}
			}
			
			function getTitleNamespace(title) {  // returns namespace number
				var prefix = /^(.+?):/.exec(title);
				if (!prefix) {
					return 0;  // no prefix means article
				}
				return mw.config.get('wgNamespaceIds')[ prefix[1].toLowerCase().replace(/ /g, '_') ] || 0;
			}
			
			function getLinkTitle($link) {  // gets title for unwatch/watch links & common page links
				// Titles can be absent for .mw-changeslist-title elements because of Popups extension.
				var title = $link.filter('.mw-changeslist-title').attr('title');
				
				if (!title) {
					title = mw.util.getParamValue('title', $link.attr('href'));
					title = title && title.replace(/_/g, ' ');
				}
				
				if (!title) {
					title = $link.filter('.mw-changeslist-title').text();
				}
				
				return title;
			}
			
			function zzz(s) {  // 5 -> 005
				s = s.toString();
				if (s.length === 1) {
					return '00' + s;
				} else if (s.length === 2) {
					return '0' + s;
				} else {
					return s;
				}
			}
			
			
			/* MAIN BLOCK */
			
			// add 20 seconds just in case (for example, the scripting could take too much)
			whenPageLoadedOrUpdated = Number(new Date()) - 20000;
			
			sortingDone = false;
			allStarsShown = false;
			hoverOnTitlesDone = false;
			
			// UNWATCH LINKS
			// on every line
			if (document.cookie.includes('wlunw=1')) {
				showAllStars();
			} else {
				bindHoverOnTitles();  // mouseover on title
			}
			
			if (firstRun) {
				if (document.cookie.includes('wlrvoff=1')) {
					switchRevert();
				}
				
				// find insertion point for links
				$linksIn = $('<div>')
					.addClass('mw-rcfilters-ui-cell gadgetWatchlist-linksContainer')
					.insertBefore($(
						mw.user.options.get('rcenhancedfilters') ?
							'.mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' :
							'.wlinfo'
					));
				
				// "show all stars" link
				addLink('', strings.unwatchTip, 'gadgetWatchlist-icon gadgetWatchlist-icon-unwatched')
					.click(showAllStars);
				
				// FUNCTION LINKS
				// "sort" link
				addLink('↑↓', strings.sortTip).click(sortWatchlist);
				
				// "expand all" link
				// Auto-update could be used, so even if there is no $('.mw-enhancedchanges-arrow') elements, we keep
				// the button.
				if (isEnhanced) {
					addLink('±', strings.expandAll).click(expandMultipleEdits);
				}
				
				// "switch revert" link
				if (mw.config.get('wgUserGroups').indexOf('rollbacker') !== -1) {
					addLink('⎌', strings.switchRevert).click(switchRevert);
				}
				
				// "only new" link
				addLink(strings.newOnly, strings.newOnlyTip).mousedown(newEntriesOnly);
				
				
				// TABS
				if (!window.wlNoTabs) {
					var $mainTab = $('#ca-nstab-special').first();  // "Special" tab
					
					// change main tab into "watchlist Δ"
					var watchlistTitle = $.trim($('#firstHeading').text());
					$mainTab
						.find('a')  // replace "Special" with "Watchlist" as tab text
						.text(watchlistTitle + ' △')  // Δ is good but monobook makes is lowercase
						.attr('title', watchlistTitle + ' — ' + strings.newOnlyTip.toLowerCase())
						.on('mousedown keydown', newEntriesOnly);
					
					// add "hideInterface" tab
					$mainTab
						.clone(true)
							.attr('id', '')
							.attr('href', 'javascript:')
							.removeClass('selected')
							.click(hideInterface)
							.appendTo($mainTab.parent())
						.find('a')
							.text('↸')
							.attr('title', strings.fullPage)
							.attr('accesskey', '');
				}
				
				
				// OTHER TASKS
				if (document.cookie.includes('wlmax=1')) {
					hideInterface();
				}
				
				mw.util.addCSS('\
					.mw-special-Watchlist .mw-changeslist .mw-rollback-link.mw-rollback-link {\
						visibility: visible;\
					}\
				');
				
				firstRun = false;
			}
			
			return;
		}
		
		// Occurs in watchlist when mediawiki.rcfilters.filters.ui module for some reason fires
		// wikipage.content for the second time with an element that is not in the DOM,
		// fieldset#mw-watchlist-options (in mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelUpdate
		// function).
		if (!$content.parent().length) return;
		
		// Recent changes type in preferences. Don't confuse enhanced recent changes with enhanced filters.
		// mw.user.options.get('usenewrc') shouldn't be used here: RC mode could be set from URL.
		var isEnhanced = !$content.find('ul.special').length;
		
		if (mw.user.options.get('rcenhancedfilters')) {
			mw.hook('structuredChangeFilters.ui.initialized').add(main);
		} else {
			if (!navigator.userAgent.toLowerCase().includes('firefox')) {
				main();
			} else {
				// Cure for Firefox
				setTimeout(main, 0);
			}
		}
	});
}

})();