MediaWiki:Gadget-catMoveLink.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at MediaWiki:Gadget-catMoveLink. |
/**
* catMoveLink
* UI Gadget for moving category contents
*
*
* @rev 1 (2014-05-16)
* @author Rillke <https://commons.wikimedia.org/wiki/User:Rillke>, 2014
* @license This software is quadruple licensed. You may use it under the terms of GPL v.3, LGPL v.3, CC-By-SA 3.0, GFDL 1.2
*
**/
/*global jQuery:false, mediaWiki:false, alert:false */
(function($, mw) {
'use strict';
var messages = {
'com-cml-title': "$1",
'com-cml-reason': "Reason and edit summary",
'com-cml-reason-hint': "Reason with at least {{plural:$1|$1 character|$1 characters}}",
'com-cml-target': "Target – The new desired category name",
'com-cml-button-submit': "Run selected action",
'com-cml-button-cancel': "Cancel",
'com-cml-first-step': "select action",
'com-cml-failed-loading-options': "Error retrieving options and translation",
'com-cml-add-to-talk': "Read the talk page and comment there",
'com-cml-add-to-talk-info': "Good for a relaxed discussion and to see which thoughts other users shared about the category before.",
'com-cml-newtalk': "Start a new talk page",
'com-cml-newtalk-info': "Good for a relaxed discussion.",
'com-cml-nominate-for-discussion': "Nominate category for discussion",
'com-cml-nominate-for-discussion-info': "Starts the formal CfD process.",
'com-cml-add-move-template': "Suggest moving on the category page itself",
'com-cml-add-move-template-info': "Be specific and make sure you have a good reason. An administrator will finally decide over it."
};
mw.messages.set(messages);
var msg = function(/*params*/) {
var args = Array.prototype.slice.call(arguments, 0);
args[0] = 'com-cml-' + args[0];
args[args.length] = 'Category Move Assistant (Community Script)';
return mw.message.apply(this, args).parse();
};
var css = [
'#com-cml-options-table tr:not(:first-child) { cursor: pointer }',
'.com-cml-input-block { width: 100%; box-sizing: border-box; display: block; }',
'.com-cml-optioncontainer { margin-top: 1.5em }',
'.com-cml-sub-nav { font-size: smaller }',
'.com-cml-sub-nav span, .com-cml-sub-nav a { display: inline-block; overflow: hidden; white-space: nowrap; }',
'.com-cml-ellipsis-on-overflow { text-overflow: ellipsis; max-width: 90%; } ',
'.com-cml-optioncontainer-option { padding: 0.4em 0; }',
'#com-cml-dummy-submit { visibility: hidden; }'
];
var mwconfig = mw.config.get([
'wgNamespaceNumber',
'wgUserGroups',
'wgGlobalGroups',
'wgIsProbablyEditable',
'wgTranslatePageTranslation',
'wgTitle'
]);
var stack,
$doc = $(document),
$win = $(window);
var KEY_SPACE = 32,
KEY_ENTER = 13,
KEY_UP = 38,
KEY_DOWN = 40;
$.fn.comIsValid = function() {
var v = true;
$(this).find('input').each(function() {
var $input = $(this),
isRequired = $input.is('[required]'),
pattern = $input.attr('pattern'),
isnot = $input.attr('isnot'),
val = $input.val();
if (isRequired) {
v = v && ( isRequired && val );
if (!v) return false;
}
if (pattern) {
v = v && new RegExp(pattern).test(val);
if (!v) return false;
}
if (isnot) {
v = v && ( val !== isnot );
if (!v) return false;
}
});
return v;
};
/**
* @singleton
*/
var moveLink = {
config: {
fatalErrorReportURL: 'https://commons.wikimedia.org/wiki/Commons:Administrators%27_noticeboard',
hookOnSelector: '#ca-move',
optionsTablePage: 'Commons:Categories/MoveOptions/render'
},
jStackOptions: {
app: 'Category Move Assistant',
reportPage: 'MediaWiki talk:Gadget-Category Move Assistant.js/errors'
},
stack: null,
install: function() {
// In category namespace only
// if ( mwconfig.wgNamespaceNumber !== 14 ) return;
// For registred users only
if ( $.inArray( 'user', mwconfig.wgUserGroups ) === -1 ) return;
// If we are not allowed to edit this page, we can't move it
if ( !mwconfig.wgIsProbablyEditable ) return;
// If the page is translated by the translate extension, don't allow non TA's to move the category's contents
if ( mwconfig.wgTranslatePageTranslation && $.inArray( 'translationadmin', mwconfig.wgUserGroups ) === -1 ) return;
var $moveLink = this.$moveLink = $(moveLink.config.hookOnSelector);
if ($moveLink.length) {
$moveLink.click(function(e) {
e.preventDefault();
moveLink.loadInterface($moveLink);
});
mw.util.addCSS(css.join('\n'));
}
},
isUserBot: function() {
return $.inArray( 'bot', mwconfig.wgUserGroups ) !== -1 ||
$.inArray( 'Global_bot', mwconfig.wgGlobalGroups ) ||
$.inArray( 'steward', mwconfig.wgGlobalGroups ) !== -1;
},
loadInterface: function($moveLink) {
moveLink.loadReasons();
mw.loader.using([
'ext.gadget.libJQuery',
'ext.gadget.progressDialog',
'ext.gadget.libCat',
'ext.gadget.JStack',
'ext.gadget.jquery.blockUI',
'jquery.throttle-debounce',
'jquery.ui',
'jquery.lengthLimit',
'jquery.spinner',
'jquery.ui'
], moveLink.waitReasons, function() {
// The last time we're using alert in this script
// but if components aren't loaded alert is the safest
// bringing something to the user's attention
// since alert is modal, never alert on each page load
alert(
"An error occurred loading the required components of catMoveLink." +
"Please report this issue on " + moveLink.config.fatalErrorReportURL
);
$moveLink.off('click').click();
});
},
loadReasons: function() {
var processResult = function(r) {
// This is considered safe because the text generated by action=render
// is created by the MediaWiki parser
moveLink.$optionsPage = $('<div>' + r + '</div>');
moveLink.$optionsTable = moveLink.$optionsPage.find('#com-cml-options-table');
moveLink.$translations = moveLink.$optionsPage.find('#com-cml-translations');
moveLink.readTranslation(moveLink.$translations);
moveLink.$reasonLoadStatus.resolve();
};
// No need to fetch again but make sure we start clean
if (moveLink.optionsPageRaw) return processResult(moveLink.optionsPageRaw);
moveLink.$reasonLoadStatus = $.Deferred();
$.get(mw.util.wikiScript(), $.param({
'action': 'render',
'title': moveLink.config.optionsTablePage
})).done(function(r) {
r = r.replace('$cat', mwconfig.wgTitle);
moveLink.optionsPageRaw = r;
processResult(r);
}).error(function() {
moveLink.$reasonLoadStatus.reject();
});
},
readTranslation: function($translations) {
$.each(messages, function(k, v) {
// Since we only run this on demand, it should be sufficiently fast
messages[k] = $translations.find('#' + k).text() || v;
});
mw.messages.set(messages);
},
waitReasons: function() {
moveLink.stack = stack = new mw.libs.JStack( moveLink );
if (!moveLink.$reasonLoadStatus) moveLink.loadReasons();
moveLink.$reasonLoadStatus
.done(moveLink.showInterface)
.fail(function() {
stack.showErrDlg(new Error(msg('failed-loading-options')), {
showRetry: false,
showIgnore: false
});
});
},
showInterface: function() {
var $dButtons, $submitButton,
buttons = {},
$ui = moveLink.$getUI(),
canSubmit = false;
buttons[msg('button-submit')] = function() {
$ui.submit();
};
buttons[msg('button-cancel')] = function() {
$(this).dialog('close');
};
$ui.submit(function() {
if (!canSubmit) return;
$ui.dialog('widget').block({
message: $.createSpinner({
size: 'large',
type: 'block'
})
});
setTimeout(function() {
$doc.triggerHandler('catMoveLink.submit.submit');
// Do not close the dialog before obtaining the information required!
$(this).dialog('close');
}, 500);
});
$ui.dialog({
title: msg('title'),
buttons: buttons,
width: Math.min($win.width(), 800),
modal: true,
close: function() {
// Destroy dialog on close
$(this).remove();
},
open: function() {
var height;
$dButtons = $ui.parent().find('.ui-dialog-buttonpane').find('button');
$submitButton = $dButtons
.eq(0)
.specialButton('proceed')
.button({
disabled: true
});
$doc.on({
'catMoveLink.submit.enable': function() {
$submitButton.button('option', 'disabled', false);
canSubmit = true;
},
'catMoveLink.submit.disable': function() {
$submitButton.button('option', 'disabled', true);
canSubmit = false;
}
});
$dButtons.eq(1).specialButton('cancel');
moveLink.$optionsTable.find('tr').eq(1).focus();
height = Math.min($ui.dialog('widget').height(), $win.height());
$ui.dialog('option', 'height', height);
}
});
},
table2Select: function($table) {
var $trs,
$changeCbs = $.Callbacks();
$table
.find('table')
.addClass('ui-widget-content');
// Override the method from .prototype (how evil ...)
$table.change = function(cb) {
$changeCbs.add(cb);
};
$trs = $table.find('tr').not('tr:first').hover(function() {
$(this).addClass('ui-state-default');
}, function() {
$(this).removeClass('ui-state-default');
}).click(function() {
var $tr = $(this);
$trs.removeClass('ui-state-highlight');
$tr.addClass('ui-state-highlight');
$tr.focus();
$table.value = $tr.attr('id');
$changeCbs.fire($tr, $table.value);
})
.focus(function() {
var $tr = $(this);
$trs.removeClass('ui-state-default');
$tr.addClass('ui-state-default');
})
.attr('tabindex', 0)
.keyup(function(e) {
var $next, $prev,
$tr = $(this);
e.preventDefault();
switch(e.which) {
case KEY_SPACE:
case KEY_ENTER:
$tr.click();
break;
case KEY_UP:
$prev = $tr.prev('tr');
if ($prev.length === 0 || $prev.find('th').length) $prev = $trs.last();
$prev.focus();
break;
case KEY_DOWN:
$next = $tr.next('tr');
if ($next.length === 0) $next = $trs.first();
$next.focus();
break;
}
});
return $table;
},
tasks: {
discuss: function() {
},
page: function() {
},
content: function() {
},
redir: function() {
},
immediately: function() {
},
oAuth: function() {
},
CommonsDelinkerAdmin: function() {
},
CommonsDelinker: function() {
}
},
uiElements: {
getBackLink: function($option, cb) {
var $subNav = $('<div>').addClass('com-cml-sub-nav');
$('<a>')
.attr('href', '#back')
.text(msg('first-step'))
.click(function(e) {
e.preventDefault();
cb();
})
.appendTo($subNav);
$('<span>')
.text('> ')
.appendTo($subNav);
$('<span>')
.text($option.find('td').first().text())
.addClass('com-cml-ellipsis-on-overflow')
.appendTo($subNav);
return $subNav;
},
reasonCounter: 0,
reasonAndTarget: function() {
var $rNt = $('<div>').addClass('com-cml-optioncontainer');
$rNt.$reasonLabel = $('<label>')
.attr({
'for': 'com-cml-reason-input-' + this.reasonCounter,
'class': 'com-cml-input-block'
})
.text(msg('reason'))
.appendTo($rNt);
$rNt.$reasonInput = $('<input type="text" />')
.attr({
'id': 'com-cml-reason-input-' + this.reasonCounter,
'maxlength': 255,
'placeholder': msg('reason'),
'class': 'com-cml-input-block',
'title': msg('reason-hint', 5),
'required': 'required',
'pattern': '.{5,}'
})
.byteLimit(255)
.appendTo($rNt);
$rNt.$targetLabel = $('<label>')
.attr({
'for': 'com-cml-target-input-' + this.reasonCounter,
'class': 'com-cml-input-block'
})
.text(msg('target'))
.appendTo($rNt);
$rNt.$targetInput = $('<input type="text" />')
.attr({
'id': 'com-cml-target-input-' + this.reasonCounter,
'maxlength': 255,
'placeholder': msg('target'),
'class': 'com-cml-input-block',
'required': 'required',
'isnot': mwconfig.wgTitle
})
.val(mwconfig.wgTitle)
.byteLimit(255)
.appendTo($rNt);
this.reasonCounter++;
return $rNt;
},
discuss: function() {
// Check if there's a talk page
var makeOptionGroup, validate, radioVal,
$optionDiv,
$allDetails = $(),
$allRadios = $(),
$reasonAndTarget = moveLink.uiElements.reasonAndTarget(),
$talkTab = $('#ca-talk'),
$discussionLink = $('#t-ajaxquickdiscusscat'),
hasTalk = !$talkTab.hasClass('new') && !$talkTab.find('.new').length;
if (!$discussionLink.length) {
// Prefetch AQD
mw.loader.load('ext.gadget.AjaxQuickDelete');
}
$optionDiv = $('<div>')
.addClass('com-cml-optioncontainer')
.attr('id', 'com-cml-opt-discuss-wrap');
makeOptionGroup = function(val, mwmsg, $content) {
var $radio,
$details,
$wrap = $('<div>')
.attr('id', 'com-cml-opt-discuss-' + val + '-wrap')
.addClass('com-cml-optioncontainer-option')
.appendTo($optionDiv);
$radio = $('<input name="com-cml-opt-discuss" id="com-cml-radio-opt-' + val + '" value="' + val + '" type="radio" />').appendTo($wrap);
$allRadios = $allRadios.add($radio);
$('<label for="com-cml-radio-opt-' + val + '"></label>').text( msg(mwmsg) ).appendTo($wrap);
$details = $('<div>')
.attr('id', 'com-cml-opt-discuss-' + val + '-details')
.addClass('com-cml-opt-discuss-details')
.hide()
.appendTo($wrap);
$allDetails = $allDetails.add($details);
$('<p>').text( msg(mwmsg + '-info') ).appendTo($details);
$details.append($content);
};
// TODO: This could be a guided tour.
makeOptionGroup('talk', hasTalk ? 'add-to-talk' : 'newtalk');
makeOptionGroup('nfd', 'nominate-for-discussion');
makeOptionGroup('tag', 'add-move-template', $reasonAndTarget);
// Interactivity
validate = function() {
if (({ 'talk': 1, 'nfd': 1 })[radioVal]) {
$doc.triggerHandler('catMoveLink.submit.enable');
} else {
if ($reasonAndTarget.comIsValid()) {
$doc.triggerHandler('catMoveLink.submit.enable');
} else {
$doc.triggerHandler('catMoveLink.submit.disable');
}
}
};
$allRadios.change(function() {
var $radio = $(this);
// Update variable that is used by other functions
radioVal = $radio.val();
$allRadios.not(this).nextAll('div.com-cml-opt-discuss-details').stop(true).slideUp();
$radio.nextAll('div.com-cml-opt-discuss-details').slideDown();
// Validate input
validate();
});
$reasonAndTarget
.find('input')
.on('keyup change input', mw.util.debounce( 50, validate ) );
// TODO: Enable buttons
$optionDiv.getValues = function() {
return {
action: $allRadios.find('checked').val(),
reason: $reasonAndTarget.$reasonInput.val(),
target: $reasonAndTarget.$targetInput.val()
};
};
return $optionDiv;
},
page: function() {
},
content: function() {
},
redir: function() {
},
immediately: function() {
},
oAuth: function() {
},
CommonsDelinkerAdmin: function() {
},
CommonsDelinker: function() {
}
},
$getUI: function() {
var $ui = $('<form>'),
$table = moveLink.$optionsTable;
// This allows the form to be actually submitted and validated
$('<input id="com-cml-dummy-submit" type="submit" value="dummy" style="position:absolute"/>')
.height(0)
.width(0)
.appendTo($ui);
$table = $ui.$table = moveLink.table2Select($table);
$table.appendTo($ui);
$table.change(function($el, val) {
// and drop the table
$table.effect('drop', function() {
// Create the new content
var $backLink,
$newContent = stack.secureCall(moveLink.uiElements[val.replace('com-cml-option-move-', '')]);
if (!$newContent) {
alert("This feature has not been implemented, yet.");
$table.show('drop', 'slow');
return;
}
// Obtain the "back-to-options-table-link"
$backLink = moveLink
.uiElements
.getBackLink($el, function() {
$backLink.remove();
// Hide the new content and show table again
$newContent.effect('drop', {
direction: 'right'
}, function() {
$table.show('drop', 'slow', function() {
$el.focus();
});
$newContent.remove();
});
})
.appendTo($ui);
$backLink.find('a').focus();
// Show the new content
$newContent.hide().appendTo($ui).show('drop', {
direction: 'right'
}, 'slow');
});
});
$ui.submit(function(e) {
e.preventDefault();
});
return $ui;
}
};
window.catMoveLink = moveLink;
$(moveLink.install);
}(jQuery, mediaWiki));