/**
* 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, '')
// 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 [[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'));
});
}
// 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);
}
});
// </nowiki>