User:SD0001/StubSorter.js

/** * Ajax-based stub tag manager * * See User:SD0001/StubSorter for details and installation instructions. * */

// // 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(		$(' ').attr('id', 'stub-sorter-wrapper').css({ 'max-height': 'max-content', 'background-color': '#c0ffec', 'margin-bottom': '10px' }).append( $(' ')				.attr('id', 'stub-sorter-select') .attr('multiple', 'true') .change(handlePreview),

$(' ').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( $(' ').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(

$(' ').append(

// Freeze button $(' ').append(				$('').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 $(' ').append(				$(' ').text('List prefix matches first').val('prefix'),				$(' ').text('List intitle matches first').val('intitle'),				$(' ').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 $(' ').append(				' (', $('').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 $(' ')		.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(					$(' ').text(stub).val(stub)				); });

// We've changed the, 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(				$(' ')					.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 = '\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 '';	}); // Automatically remove if accidentally left behind if (tagsAfter.length > 1) { var idx = tagsAfter.indexOf(''); if (idx !== -1) { tagsAfter.splice(idx, 1); }	}

// remove all stub tags pageText = pageText .replace(/\{\{[^{ ]*[sS]tub(\|.*?)?\}\}\s*/g, '') // also remove tags with spaces, but don't try to remove any params .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 StubSorter', nocreate: 1, minor: getPref('minor', true), watchlist: getPref('watchlist', 'nochange') }; };

var handleSave = function submit { $('#stub-sorter-error').remove; var $status = $(' ').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'));	}); }

// Enable activation from other scripts mw.hook('StubSorter_activate').add(activate); window.StubSorter_create_edit = createEdit;

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

});

//