MediaWiki:Gadget-watchlist.js

Материал из Википедии — свободной энциклопедии
Это старая версия этой страницы, сохранённая Jack who built the house (обсуждение | вклад) в 14:35, 9 апреля 2018 (да, какое-то время экономится; + блок ссылок в СН с новыми фильтрами; в СН без новых фильтров — переносим в правый верхний угол блока настроек, чтобы не болталось посреди посторонних элементов; всплывающие подсказки для иконок «следить» / «не следить»; исправление логики в showAllStars; семантичные названия и кодстайл). Она может серьёзно отличаться от текущей версии.
Перейти к навигации Перейти к поиску
JS-код ниже относится к гаджету «В списке наблюдения: только новые изменения, сортировка списка, мгновенное „не следить“, возможность прятать кнопку отката» (править описание). Его использует около 3700 учётных записей.

После сохранения или недавних изменений очистите кэш браузера.

var gadgetWatchlistFirstRun = true;

if (mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist' &&
	mw.config.get('wgAction') === 'view'
) {
	mw.hook('wikipage.content').add(function ($content) {
		function main() {
			// <<<

/* MAIN BLOCK */
var whenPageLoaded = +(new Date()) - 20000;  // add 20 seconds just in case

// In a watchlist with the new filters on, the second selector is present on subsequent auto-updates.
var TITLE_LINK_SELECTOR = 'a[href^="/wiki/"]:first, a[href^="//ru.wikipedia.org/wiki/"]:first';

var hideInterfaceCSS, rvOffCSS;
var sortingDone, allStarsOn, hoverOnTitlesDone;
var hideInterfaceClingedToHook = false;

// ruwiki strings
var strings = {
	watch: 'Следить',
	unwatch: 'Не следить',
	sortTip: 'Сортировать изменения по пространствам имён',
	sortDone: 'Изменения уже отсортированы',
	unwatchTip: 'Добавить звёздочки для вычёркивания страниц из списка наблюдения',
	onlynew: 'Только новые',
	onlynewTip: 'Изменения с момента загрузки этой страницы',
	onlynewTipLowerCase: 'изменения с момента загрузки этой страницы',
	expandAll: 'Показать/спрятать все свёрнутые правки',
	switchRevert: 'Спрятать/показать ссылки «откатить»',
	fullPage: 'Спрятать/показать элементы интерфейса'
};

// Recent changes type in preferences. Don't confuse enhanced recent changes with enhanced filters.
var isEnhanced = !$content.find('ul.special').length;

var $linksIn;
if (!$('.gadgetWatchlist-linksContainer').length) {
	// find insertion point for links
	$linksIn = $('<div>')
		.addClass('gadgetWatchlist-linksContainer mw-rcfilters-ui-cell')
		.insertBefore($(
			mw.user.options.get('rcenhancedfilters') ?
				'.mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' :
				'.wlinfo'
		));
}

// UNWATCH LINKS
// on every line
if (document.cookie.includes('wlunw=1')) {
	showAllStars();
} else {
	bindHoverOnTitles();  // mouseover on title
}
// switch for above, saved in a cookie

// click on timestamp (enhanced recent changes) / empty space (non-enhanced)
addLink('', strings.unwatchTip, 'gadgetWatchlist-icon gadgetWatchlist-icon-unwatched').click(showAllStars);

if (document.cookie.includes('wlrvoff=1')) {
	switchRevert();
}

if (gadgetWatchlistFirstRun) {
	// OTHER LINKS
	// "sort" link
	addLink('↑↓', strings.sortTip).click(sortWatchlist);
	
	// "expand all" link
	if ($('.mw-enhancedchanges-arrow').length) {
		addLink('±', strings.expandAll).click(expandMultipleEdits);
	}
	
	// "switch revert" link
	if ($('.mw-rollback-link').length) {
		addLink('⎌', strings.switchRevert).click(switchRevert);
	}
	
	// "only new" link
	addLink(strings.onlynew, strings.onlynewTip).mousedown(onlyNewEntries);
	
	
	// TABS
	if (!window.wlNoTabs) {
		var $mainTab = $('#ca-special, #ca-nstab-special').first();  // "Special" tab
		
		// change main tab into "watchlist Δ"
		var watchlistTitle = $.trim($('h1#firstHeading').text());
		$mainTab
			.find('a')  // replace "Special" tab text with "Watchlist"
			.text(watchlistTitle + ' △')  // Δ is good but monobook makes is lowercase
			.attr('title', watchlistTitle + ' — ' + strings.onlynewTipLowerCase)
			.on('mousedown keydown', onlyNewEntries);
		
		// add "hideInterface" tab
		$mainTab
			.clone(true)
				.removeClass('selected')
				.appendTo($mainTab.parent())
				.click(hideInterface)
				.attr('id', '')
				.attr('href', '#')
			.find('a')
				.text('↸')
				.attr('title', strings.fullPage)
				.attr('accesskey', '');
	}
	
	
	// OTHER TASKS
	if (document.cookie.includes('wlmax=1')) {
		hideInterface();
	}
	
	if ($('.mw-changeslist .mw-rollback-link').first().css('visibility') === 'hidden') {
		mw.util.addCSS('\
			.gadgetWatchlist-initialized .mw-special-Watchlist .mw-changeslist .mw-rollback-link {\
				visibility: visible;\
			}\
		');
	}
}

$('html').addClass('gadgetWatchlist-initialized');

gadgetWatchlistFirstRun = false;

return;


/* FUNCTIONS */
function addLink(contents, tip, classes) {
	return $('<a>')
		.attr('href', 'javascript:')
		.attr('title', tip)
		.addClass(classes)
		.append(contents)
		.appendTo($linksIn);
}

function onlyNewEntries(e) {
	var url = window.location.href.split('#')[0];
	var days = (+(new Date()) - whenPageLoaded)/(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();
	}
	if (!allStarsOn) {
		$content.find('a[href*="action=history"]')
			.each(function(i, link) {
				updateStar(getRow($(this)));
			});
		if (e) {
			var cookieDate = new Date($.now() + 1000 * 60 * 60 * 24 * 90).toGMTString();
			document.cookie = 'wlunw=1; expires=' + cookieDate + '; path=/';
		}
		allStarsOn = true;
	} else {  // otherwise remove
		var $unwatchWatchLink = $content.find('.gadgetWatchlist-unwatchWatchLink');
		if (!$unwatchWatchLink.closest('.gadgetWatchlist-icon-unwatched').length) {
			$unwatchWatchLink.remove();
		}
		document.cookie = 'wlunw=0; expires=' + (new Date()).toGMTString() + '; path=/';
		if (!hoverOnTitlesDone) {
			bindHoverOnTitles();
		}
		allStarsOn = false;
	}
}

function sortWatchlist(e) {
	e.preventDefault();
	if (sortingDone) {
		return alert(strings.sortDone);
	}
	$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));
		}
	});
	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() + 1000 * 60 * 60 * 24 * 90)).toGMTString();
		document.cookie = 'wlrvoff=1; expires=' + cookieDate + '; path=/';
	}
}

function bindHoverOnTitles() {  // find all "titles" links and assign hover event
	if (hoverOnTitlesDone) return;
	// $('#mw-content-text').find(isEnhanced ? 'table' : 'li');
	$content.find('a[href*="action=history"]')
		.each(function () {
			getRow($(this))
				.find(TITLE_LINK_SELECTOR)
				.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 $unwatchWatchLink = $(this).find('.gadgetWatchlist-unwatchWatchLink');
		if ($unwatchWatchLink.length &&
			$unwatchWatchLink.attr('href').includes('&action=unwatch') &&
			!($unwatchWatchLink.hasClass('gadgetWatchlist-unwatchWatchLink-failure') ||
				$unwatchWatchLink.hasClass('gadgetWatchlist-unwatchWatchLink-waiting')
			) &&
			!allStarsOn
		) {
			$unwatchWatchLink.remove();
		}
	});
}

function updateStar($row) {
	var $unwatchWatchLink = $row.find('.gadgetWatchlist-unwatchWatchLink');
	if (!$unwatchWatchLink.length) {  // create
		$unwatchWatchLink = $('<a>')
			.attr('href', $row
				.find('a[href*="action=history"]')
				.attr('href')
				.replace(/&curid=\d+/, ''))
			.addClass('gadgetWatchlist-unwatchWatchLink gadgetWatchlist-icon')
			.click(ajaxUnwatchWatch)
			.insertBefore($row.find(TITLE_LINK_SELECTOR));
	}
	// update
	var icon, prevIcon, action;
	if ($row.hasClass('gadgetWatchlist-unwatchedRow')) {
		icon = 'unwatched';
		prevIcon = 'watched';
		action = 'watch';
	} else {
		icon = 'watched';
		prevIcon = 'unwatched';
		action = 'unwatch';
	}
	$unwatchWatchLink
		.attr('title', mw.messages.get(action) || strings[action])
		.attr('href', $unwatchWatchLink
			.attr('href')
			.replace(/&action=\w+/, '&action=' + action))
		.removeClass('gadgetWatchlist-icon-' + prevIcon)
		.addClass('gadgetWatchlist-icon-' + icon);
}

function getRow($el) {
	return $el.closest(isEnhanced ? 'tr' : 'li');
}

function ajaxUnwatchWatch(e) {
	var $unwatchWatchLink = $(this), errorMsg = '';
	var req = {
		token: mw.user.tokens.get('watchToken'),
		title: getLinkTitle($unwatchWatchLink)
	};
	if ($unwatchWatchLink.attr('href').includes('&action=unwatch')) {
		req.unwatch = '';
	}
	$unwatchWatchLink.addClass('gadgetWatchlist-unwatchWatchLink-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') {
				unwatchSuccess(req.title, true);
			} else if (typeof resp.watch.watched === 'string') {
				unwatchSuccess(req.title, false);
			} else {
				errorMsg = 'unrecognized response';
			}
		},
		error: function(xhr, status, error) {
			errorMsg = status + ':' + error;
		},
		complete: function () {  // update unwatch link
			$unwatchWatchLink.removeClass('gadgetWatchlist-unwatchWatchLink-waiting');
			if (errorMsg) {
				$unwatchWatchLink
					.attr('title', 'API error: ' + errorMsg)
					.addClass('gadgetWatchlist-unwatchWatchLink-failure');
			} else {
				$unwatchWatchLink.removeClass('gadgetWatchlist-unwatchWatchLink-failure');
			}
		}
	});
	return false;
}

function unwatchSuccess(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
	}
	if (ns % 2) {
		ns--;
	} else {
		ns++;  // switch to "other" 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('a[href*="action=history"]').each(function () {
		var ttl = getLinkTitle($(this));
		if (ttl !== name && ttl !== name2) return;
		var $row = getRow($(this));
		$row.toggleClass('gadgetWatchlist-unwatchedRow', isUnwatched || false);
		updateStar($row);
		if (!isUnwatched && !allStarsOn) {
			$row.find('.gadgetWatchlist-unwatchWatchLink').remove();
		}
	});
}

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);
		} else {
			if (!hideInterfaceClingedToHook) {
				mw.hook('structuredChangeFilters.ui.initialized').add(function () {
					hideInterfaceCSS = mw.util.addCSS(hideInterfaceCSSCode);
					$('.mw-rcfilters-ui-markSeenButtonWidget')
						.wrap('<div>')
						.parent()
						.addClass('mw-rcfilters-ui-markSeenButtonWidget-container')
						.appendTo('.mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails');
				});
				hideInterfaceClingedToHook = true;
			}
		}
	} 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() + 1000 * 60 * 60 * 24 * 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=' part from a link
	return mw.util.getParamValue('title', $link.attr('href')).replace(/_/g, ' ');
	// var ma = /(&|\?)title=([^&]+)/.exec($link.attr('href'));
	// if (ma) {
	// 	return decodeURIComponent(ma[2]).replace(/_/g, ' ');
	// } else {
	// 	return '';
	// }
}

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

			// <<<
		}
		
		// 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;
		
			// 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;
					}
				};
			}
		
		if (!navigator.userAgent.toLowerCase().includes('firefox')) {
			main();
		} else {
			// Cure for Firefox
			setTimeout(main, 0);
		}
	});
}