Jump to content

User:Enterprisey/StubSorter-sandbox.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * Ajax-based stub tag manager
 *
 * See [[User:SD0001/StubSorter]] for details and installation instructions.
 *
 */

// <nowiki>
// jshint maxerr: 999

$.when(
	$.ready,
	mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'jquery.chosen'])
).then(function() {

var API = new mw.Api({
	ajax: { headers: { 'Api-User-Agent': '[[w:User:SD0001/StubSorter.js]]' } }
});

var activate = function(container) {

	// if already present, don't duplicate
	if ($('#stub-sorter-wrapper').length !== 0) {
		return;
	}

	container.prepend(
		$('<div>').attr('id', 'stub-sorter-wrapper').css({
			'max-height': 'max-content',
			'background-color': '#c0ffec',
			'margin-bottom': '10px'
		}).append(
			$('<select>')
				.attr('id', 'stub-sorter-select')
				.attr('multiple', 'true')
				.change(handlePreview),

			$('<div>').attr('id', 'stub-sorter-previewbox').css({
				'background-color': '#cfd8eb' // '#98b685'
				// 'border-bottom': 'solid 0.5px #aaaaaa'
			})
		)

	);

	var $select = $('#stub-sorter-select');

	var selectExistingStubTags = function($html) {
		$html.find('.stub .hlist .nv-view a').each(function(_, e) {
			var template = e.title.slice('Template:'.length);
			$select.append(
				$('<option>').text(template).val(template).attr('selected', 'true')
			);
		});
	};

	if (mw.config.get('wgCurRevisionId') === mw.config.get('wgRevisionId')) {
		// Viewing the current version of the page, no need for api call to get the page html
		selectExistingStubTags($('.mw-parser-output'));
	} else {
		// In edit/history/diff/oldrevision mode, get the page html by api call
		API.parse(new mw.Title(mw.config.get('wgPageName'))).then(function(html) {
			selectExistingStubTags($(html));
			$select.trigger('chosen:updated');
			$select.trigger('click');
			$input.focus();
		});
	}

	$select.chosen({
		search_contains: true,
		placeholder_text_multiple: 'Start typing to add a stub tag...',
		width: '100%',

		// somehow beacuse of the hacks below, the no_results_text shows up
		// when the search results are loading, and not when there are no results
		no_results_text: 'Loading results for'
	});

	var $input = $('#stub_sorter_select_chosen input');

	var menuFrozen = false;
	var searchBy = getPref('searchBy', 'prefix');

	$('#stub_sorter_select_chosen .chosen-choices').after(

		$('<div>').append(

			// Freeze button
			$('<span>').append(
				$('<a>').text('Freeze menu ').click(function() {
					menuFrozen = !menuFrozen;
					if (menuFrozen) {
						$(this).text('Unfreeze menu ');
						$(this).parent().css('font-weight', 'bold');
					} else {
						$(this).text('Freeze menu ');
						$(this).parent().css('font-weight', 'normal');
					}
					$input[0].focus();
					$input.trigger('keyup');
				}).css({
					'padding-right': '100px',
					'padding-left': '5px'
				})
			),

			// Search mode select
			$('<select>').append(
				$('<option>').text('List prefix matches first').val('prefix'),
				$('<option>').text('List intitle matches first').val('intitle'),
				$('<option>').text('Use strict character-match search').val('regex')
			).change(function(e) {
				searchBy = e.target.value;
				$input.trigger('keyup');
			}),

			// help button after the search mode select
			$('<small>').append(
				' (', $('<a>').text('help').attr('href', '/wiki/User:SD0001/StubSorter#Search_modes').attr('target', '_blank'), ')'
			)
		).css({
			'border-bottom': 'solid 0.5px #aaaaaa',
			'border-left': 'solid 0.5px #aaaaaa',
			'border-right': 'solid 0.5px #aaaaaa'
		})

	);

	// Save button
	$('<button>')
		.text('Save').css({
			'float': 'right'
		})
		.attr('id', 'stub-sorter-save')
		.attr('accesskey', 's')
		.click(handleSave)
		.insertAfter($('#stub_sorter_select_chosen .chosen-choices'));

	// hide selected items in dropdown
	mw.util.addCSS(
		'#stub_sorter_select_chosen .chosen-results .result-selected { display: none; }'
	);

	// Focus on the search box as soon as the the sorter menu loads
	// Add placeholder, because chosen's native placeholder doesn't work with a changing menu.
	// Reset the search box width to accomodate the placeholder text
	// Keep resetting whenever the input goes out of focus
	$input
		.focus()
		.attr('placeholder', 'Start typing to add a stub tag...')
		.css('width', '200px')
		.blur(function() {
			$(this).css('width', '100%');
		});

	// also reset it when an option is selected by clicking on it
	// or when clicking on the search box after the $input has become narrow (despite our best efforts...)
	$('.chosen-container').click(function() {
		$input.css('width', '100%');
	});

	// Adapted from [[User:Enterprisey/afch-master.js/submissions.js]]'s category selection menu:
	// Offer dynamic suggestions!
	// Since jquery.chosen doesn't natively support dynamic results,
	// we sneakily inject some dynamic suggestions instead.
	// Consider upgrading to select2 or OOUI to avoid these hacks
	$input.keyup(function(e) {
		var searchStr = $input.val();

		// The worst hack. Because Chosen keeps messing with the
		// width of the text box, keep on resetting it to 100%
		$input.css('width', '100%');
		$input.parent().css('width', '100%');

		// Ignore arrow keys and home/end keys to allow users to navigate through the suggestions or through the search query
		// and don't show results when an empty string is provided
		if ((e.which >= 35 && e.which <= 40) ||
			(menuFrozen && e.which !== undefined) ||
			!searchStr) {
			return;
		}

		// true when fake keyup is produced by the Freeze button
		// in this case, api limit has to be raised to 500
		var extended = e.which === undefined;

		$.when(
			searchBy !== 'regex' ? getStubSearchResults('prefix', searchStr, extended) : undefined,
			searchBy !== 'regex' ? getStubSearchResults('intitle', searchStr, extended) : undefined,
			searchBy === 'regex' ? getStubSearchResults('regex', searchStr, extended) : undefined
		).then(function(stubsPrefix, stubsIntitle, stubsRegex) {

			var stubs;
			switch (searchBy) {
				case 'prefix': stubs = uniqElements(stubsPrefix, stubsIntitle); break;
				case 'intitle': stubs = uniqElements(stubsIntitle, stubsPrefix); break;
				case 'regex': stubs = stubsRegex; break;
			}

			// Reset the text box width again
			$input.css('width', '100%');
			$input.parent().css('width', '100%');

			// If the input has changed since we started searching,
			// don't show outdated results
			if ($input.val() !== searchStr) {
				return;
			}

			// Clear existing suggestions
			$select.children().not(':selected').remove();

			// Now, add the new suggestions
			stubs.forEach(function (stub) {

				// do not add if already selected
				if ($select.val().indexOf(stub) !== -1) {
					return;
				}
				$select.append(
					$('<option>').text(stub).val(stub)
				);
			});

			// We've changed the <select>, now tell Chosen to
			// rebuild the visible list
			$select.trigger('liszt:updated');
			$select.trigger('chosen:updated');
			$input.val(searchStr);
			$input.css('width', '100%');
			$input.parent().css('width', '100%');

		}).catch(function(e) {
			if ($input.val() !== searchStr) {
				return;
			}
			$select.children().not(':selected').remove();
			$select.append(
				$('<option>')
					.text('Error fetching results: ' + e)
					.attr('disabled', 'true')
			);
			$select.trigger('liszt:updated');
			$select.trigger('chosen:updated');
			$input.val(searchStr);
			$input.css('width', '100%');
			$input.parent().css('width', '100%');
		});

	});

};

var getStubSearchResults = function(searchType, searchStr, extended) {
	var query = {
		'action': 'query',
		'list': 'search',
		'srsearch': 'incategory:"Stub message templates" ',
		'srnamespace': '10',
		'srlimit': extended ? '500' : '100',
		'srqiprofile': 'classic',
		'srprop': '',
		'srsort': 'relevance'
	};
	switch (searchType) {
		case 'prefix':
			query.srsearch += 'prefix:"Template:' + searchStr + '"';
			break;
		case 'intitle':
			var searchStrWords = searchStr.split(' ').filter(function(e) {
				return !/^\s*$/.test(e);
			});
			query.srsearch += 'intitle:"' + searchStrWords.join('" intitle:"') + '"';
			break;
		case 'regex':
			query.srsearch += 'intitle:/' + mw.util.escapeRegExp(searchStr) + '/i';
			break;
	}

	return API.get(query).then(function(response) {
		if (response && response.query && response.query.search) {
			return response.query.search.map(function(e) {
				return e.title.slice(9);
			});
		} else {
			return $.Deferred().reject(JSON.stringify(response));
		}
	}, function(e) {
		return $.Deferred().reject(JSON.stringify(e));
	});
};

var handlePreview = function() {

	// Show preview
	var $this = $(this);
	var selectedTags = $this.val();
	if (selectedTags.length) {
		var tagsWikitext = '{{' + selectedTags.join('}}\n{{') + '}}';

		API.parse(tagsWikitext).then(function(parsedhtmldiv) {

			// Do nothing if tag selection has changed since we
			// sent the parse API call, comparing lengths is enough
			if (selectedTags.length !== $this.val().length) {
				return;
			}
			$('#stub-sorter-previewbox').html(parsedhtmldiv);
		});
	} else {
		$('#stub-sorter-previewbox').empty();
	}
	// $input.css('width', '100%');  // doesn't work
};

var createEdit = function(pageText, values) {
	var tagsBefore = (pageText.match(/\{\{[^{ ]*?[sS]tub(?:\|.*?)?\}\}/g) || []).map(function(e) {
		// capitalise first char after {{
		return e[0] + e[1] + e[2].toUpperCase() + e.slice(3);
	});
	var tagsAfter = values.map(function(e) {
		return '{{' + e + '}}';
	});
	
	// Automatically remove {{Stub}} if accidentally left behind
	if (tagsAfter.length > 1) {
		var idx = tagsAfter.indexOf('{{Stub}}');
		if (idx !== -1) {
			tagsAfter.splice(idx, 1);	
		}
	}

	// remove all stub tags
	pageText = pageText.replace(/\{\{[^{ ]*[sS]tub(\|.*?)?\}\}\s*/g, '').trim();

	// add selected stub tags
	pageText += '\n\n\n' + tagsAfter.join('\n'); 	// per [[MOS:LAYOUT]]

	// For producing edit summary
	var summary = '';

	var tagsAdded = tagsAfter.filter(function(e) {
		return tagsBefore.indexOf(e) === -1;
	});
	var tagsRemoved = tagsBefore.filter(function(e) {
		return tagsAfter.indexOf(e) === -1;
	});

	tagsRemoved.forEach(function(e) {
		summary += '–' + e + ', ';
	});
	tagsAdded.forEach(function(e) {
		summary += '+' + e + ', ';
	});
	summary = summary.slice(0, -2); // remove the final ', '

	return {
		text: pageText,
		summary: summary + ' using [[User:SD0001/StubSorter|StubSorter]]',
		nocreate: 1,
		minor: getPref('minor', true),
		watchlist: getPref('watchlist', 'nochange')
	};
}

var handleSave = function submit() {
	$('#stub-sorter-error').remove();
	var $status = $('<div>').text('Fetching page...')
		.attr('id', 'stub-sorter-status')
		.css({
			'float': 'right'
		});
	$(this).replaceWith($status);
	API.edit(mw.config.get('wgPageName'), function(revision) {
		$status.text('Saving page...');
		var pageText = revision.content;
		return createEdit(pageText, $('#stub-sorter-select').val());
	}).then(function() {
		$status.text('Done. Reloading page...');
		setTimeout(function() {
			window.location.href = mw.util.getUrl(mw.config.get('wgPageName'));
		}, 500);
	}).fail(function(e) {
		$status.text('Save failed. Please try again.')
			.attr('id', 'stub-sorter-error')
			.css({
				'color': 'red',
				'font-weight': 'bold',
				'padding-right': '5px'
			});
		console.error(e); // eslint-disable-line no-console
		setTimeout(function() {
			$status.before($('#stub-sorter-save'));
			$('#stub-sorter-save').click(handleSave);
		}, 500);
	});
};

// utility function to get unique elements from 2 arrays
var uniqElements = function(arr1, arr2) {
	var obj = {}; var i;
	for (i = 0; i < arr1.length; i++) {
		obj[arr1[i]] = 0;
	}
	for (i = 0; i < arr2.length; i++) {
		obj[arr2[i]] = 0;
	}
	return Object.keys(obj);
};

// function to obtain a preference option from common.js
var getPref = function(name, defaultVal) {
	if (window['StubSorter_' + name] === undefined) {
		return defaultVal;
	} else {
		return window['StubSorter_' + name];
	}
};

/**
 ********************* SET UP *********************
 */

// auto start the script when navigating to an article from CAT:STUBS
if (mw.config.get('wgPageName') === 'Category:Stubs') {
	$('#mw-pages li a').each(function(_, e) {
		e.href += '?startstubsorter=y';
	});
}

// show only on existing articles, and my sandbox (for testing)
if ((mw.config.get('wgNamespaceNumber') === 0 ||
	mw.config.get('wgPageName') === 'User:SD0001/sandbox') &&
	mw.config.get('wgCurRevisionId') !== 0
) {
	mw.util.addPortletLink(getPref('portlet', 'p-cactions'), '#', 'Stub Sort',
	'ca-stub', 'Add or remove stub tags').addEventListener('click', function(e){
		e.preventDefault();
		activate($('#mw-content-text'));
	});
}

mw.hook('StubSorter_activate').add(activate);
window.StubSorter_create_edit = createEdit;


if (mw.util.getParamValue('startstubsorter')) {
	setTimeout(function() {
		$('#ca-stub').click();
	}, 1000);
}

});

// </nowiki>