User:Enterprisey/StubSorter-sandbox.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Enterprisey/StubSorter-sandbox. |
/**
* 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>