User:Mr. Stradivarius/gadgets/Draftify.js
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.
// <nowiki>
/*
* Draftify
*
* This gadget allows you to move a user page to a different location (usually
* the Draft namespace), notify the user, and optionally soft-block them.
*
* To install the script, add the following to your personal .js page:
importScript( 'User:Mr. Stradivarius/gadgets/Draftify.js' ); // Linkback: [[User:Mr. Stradivarius/gadgets/Draftify.js]]
* Author: Mr. Stradivarius
* Licence: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2015 Mr. Stradivarius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
mw.loader.using( [
'mediawiki.api',
'mediawiki.jqueryMsg',
'mediawiki.Title',
'mediawiki.util',
'oojs-ui'
], function () {
"use strict";
var config, ApiManager, Dialog;
/**************************************************************************
* MediaWiki config and exit check
**************************************************************************/
// A global object that stores all the page config, defaults, and user
// preferences.
config = {};
config.mw = mw.config.get( [
'wgTitle',
'wgNamespaceNumber',
'wgArticleId',
'wgFormattedNamespaces',
'wgPageContentModel',
'wgPageName',
'wgUserName',
'wgUserGroups',
'wgRelevantUserName',
] );
// If we're not going to work on the page, exit as soon as possible.
if (
config.mw.wgNamespaceNumber !== 2 && config.mw.wgNamespaceNumber !== 3 ||
config.mw.wgArticleId === 0 || // Page doesn't exist
!config.mw.wgRelevantUserName || // Userspace of non-existent user
config.mw.wgUserName === config.mw.wgRelevantUserName || // User's own userspace
config.mw.wgPageContentModel !== 'wikitext' || // Exclude user JS/CSS
mw.util.getParamValue( 'redirect', window.location.href ) === 'no' // Current page is a redirect
) {
return;
}
/**************************************************************************
* Messages
*
* This is the part you need to edit to localise the gadget.
**************************************************************************/
config.defaultMessages = {
// The label on the portlet link
'dfy-portlet-label': 'Draftify',
// The portlet link tooltip text
'dfy-portlet-tooltip': 'Move this draft and notify the user',
// The prefix that target draft pages must start with.
'dfy-draft-prefix': 'Draft:',
// The edit summary to use when moving the page.
'dfy-move-summary': 'the [[WP:DRAFTS|Draft namespace]] is the preferred location for [[WP:AFC|Articles for Creation]] submissions',
// The template invocation to use when tagging drafts.
// $1 - the username of the user whose userspace the draft was in
'dfy-tag-template': '{{subst:AFC draft|1=$1}}',
// The edit summary to use when tagging drafts with dfy-tag-template.
// $1 - the username of the user whose userspace the draft was in
'dfy-tag-summary': 'Tag page as draft belonging to [[:User:$1]]',
// The template invocation to use when soft-blocking users.
'dfy-softblock-template': '{{subst:uw-softerblock|sig=yes}}',
// The edit summary to use when soft-blocking users.
'dfy-softblock-summary': '{{uw-softerblock}} <!-- Promotional username, soft block -->',
// The template invocation to use when notifying users that their
// draft has been moved.
// $1 - The new page name after the move
// $2 - The current page name
'dfy-notify-template': '{{subst:uw-draftmoved|1=$1|from=$2}} ~~~~',
// The template invocation to use when suggesting the user should
// change their username.
'dfy-suggestrename-template': '{{subst:uw-username|1=it gives the impression that your account represents a group, organization or website}} ~~~~',
// Edit summary to leave when notifying the user that their draft has
// been moved.
'dfy-notify-summary-moveonly': 'Your draft page has been moved',
// Edit summary to leave when notifying the user that their draft has
// been moved and suggesting that they rename their account.
'dfy-notify-summary-suggestrename': 'Your draft page has been moved, and you may need to change your [[WP:U|username]]',
// Edit summary to leave when notifying the user that their draft has
// been moved and that they have been soft-blocked.
'dfy-notify-summary-softblock': 'Your draft page has been moved, and you have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website',
// Boilerplate text to add at the end of the edit summary.
'dfy-summary-suffix': '([[WP:DFY|DFY]])',
// The talk heading to be used for the talkpage notification.
// $1 - The current month name, as defined in config.months.
// $2 - The current year
'dfy-talk-heading': '$1 $2',
// Label for the text input where the user specifies the new draft name.
'dfy-title-input-label': 'New draft name',
// Label for the redirect checkbox
'dfy-redirect-checkbox-label': 'Leave a redirect behind',
// Label for the notification checkbox
// $1 - The user who the draft belongs to
'dfy-notify-checkbox-label': 'Notify the user of the page move',
// Label for the checkbox for adding advice about renames
// $1 - The user who the draft belongs to
'dfy-suggestrename-checkbox-label': 'Suggest that the user change their username',
// Label for the redirect checkbox
'dfy-softblock-checkbox-label': 'Soft-block the user and leave a block notice',
// Label for the watch user talk checkbox
'dfy-watch-user-talk-checkbox-label': 'Watch user talk page',
// Label for the watch draft checkbox
'dfy-watch-draft-checkbox-label': 'Watch source page and target page',
// Label for the move progress indicator
'dfy-move-progress-label': 'Moving draft...',
// Label for the tag progress indicator
'dfy-tag-progress-label': 'Tagging draft...',
// Label for the softblock progress indicator
// $1 - the user being blocked
'dfy-softblock-progress-label': 'Soft-blocking $1...',
// Label for the notify progress indicator
// $1 - the user being notified
'dfy-notify-progress-label': 'Notifying $1...',
// Label for the progress indicator for opening the talk page
// $1 - the talk page being opened
'dfy-open-talk-progress-label': 'Opening user talk...',
// Value for completed progress indicators.
'dfy-progress-success': 'Done.',
// Value for failed progress indicators.
// $1 - the error message
'dfy-progress-failed': 'Error: $1',
// Value for failed progress indicators with unknown errors.
'dfy-progress-unknown-error': 'An unknown error occurred.',
// Error message for non-existent pages
// $1 - the page name
'dfy-nonexistent-page-error': 'The page "$1" does not exist.',
};
config.months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
/**************************************************************************
* Config
*
* Get user preferences, set messages, and deal with other per-user config.
**************************************************************************/
// Get the raw user preferences.
config.rawPrefs = typeof window.Draftify === 'object' ? window.Draftify : {};
// These messages are configurable by the user.
config.configurableMessages = {
movesummary: 'dfy-move-summary',
tagtemplate: 'dfy-tag-template',
tagsummary: 'dfy-tag-summary',
notifytemplate: 'dfy-notify-template',
notifysummarymoveonly: 'dfy-notify-summary-moveonly',
notifysummarysuggestrename: 'dfy-notify-summary-suggestrename',
notifysummarysoftblock: 'dfy-notify-summary-softblock',
softblocktemplate: 'dfy-softblock-template',
softblocksummary: 'dfy-softblock-summary',
suggestrenametemplate: 'dfy-suggestrename-template'
};
// Get the messages to use from the defaults and the user preferences.
config.messages = {};
$.each( config.defaultMessages, function ( key, value ) {
config.messages[ key ] = value;
} );
$.each( config.configurableMessages, function ( prefKey, messageKey ) {
if ( typeof config.rawPrefs[ prefKey ] === 'string' ) {
config.messages[ messageKey ] = config.rawPrefs[ prefKey ];
}
} );
// Set the messages for use by the MediaWiki message library.
mw.messages.set( config.messages );
// Define the default values for non-messages that can be set as
// preferences.
config.defaults = {
redirect: false,
notify: true,
softblock: false,
suggestrename: false,
watchusertalk: true,
watchdraft: true,
menulocation: 'p-cactions',
menuposition: null
};
// Define aliases for config keys.
config.aliases = {
watchdraft: [ "watch" ] // For backwards compatibility with old options
}
// Find the preferences to use from the defaults, aliases, and user preferences.
config.prefs = {};
$.each( config.defaults, function ( key, value ) {
if ( config.rawPrefs[ key ] !== undefined ) {
config.prefs[ key ] = config.rawPrefs[ key ];
} else if ( config.aliases[ key ] !== undefined ) {
$.each( config.aliases[ key ], function ( index, alias ) {
if ( config.rawPrefs[ alias ] !== undefined ) {
config.prefs[ key ] = config.rawPrefs[ alias ];
return false; // Break the loop if we find an alias
}
} );
}
if ( config.prefs[ key ] === undefined ) {
config.prefs[ key ] = value;
}
} );
/**************************************************************************
* ApiManager class
*
* This class is the interface to the MediaWiki API and the config. Other
* classes should go through the ApiManager rather than access the config
* directly.
**************************************************************************/
ApiManager = function () {
var currentUserIsAdmin;
this.api = new mw.Api();
this.currentSubpage = config.mw.wgTitle.replace( /^.*\//, '' );
this.currentDraftTitle = new mw.Title(
config.mw.wgTitle,
config.mw.wgNamespaceNumber
);
this.targetDraftTitle = null;
this.userTalkTitle = new mw.Title( this.getTargetUser(), 3 );
};
OO.initClass( ApiManager );
// Used to validate the title field
ApiManager.static.titleValidationRegex = /^[^|<>{}\[\]#]+$/;
ApiManager.prototype.getPreference = function ( key ) {
return config.prefs[ key ];
};
// Fetches the current user's rights from the API and stores the relevant
// ones in the ApiManager object. We don't check for things
// like page protection status, so having the right to do something doesn't
// mean that doing it will be successful.
// Returns a jQuery.promise object.
ApiManager.prototype.setRights = function () {
var apiManager = this;
return mw.user.getRights().then(
// Done filter
function ( rights ) {
apiManager.rights = {
suppressredirect: $.inArray( 'suppressredirect', rights ) !== -1,
softblock: $.inArray( 'block', rights ) !== -1
};
},
// Fail filter
function () {
apiManager.rights = {};
}
);
};
ApiManager.prototype.currentUserCan = function ( action ) {
var right = this.rights[ action ];
if ( right === undefined ) {
return true;
} else {
return right;
}
};
ApiManager.prototype.getCurrentSubpage = function () {
return this.currentSubpage;
};
ApiManager.prototype.getTargetUser = function () {
return config.mw.wgRelevantUserName;
};
ApiManager.prototype.getTitleValidationRegex = function () {
return this.constructor.static.titleValidationRegex;
};
ApiManager.prototype.getCurrentDraftTitle = function () {
return this.currentDraftTitle;
};
ApiManager.prototype.getTargetDraftTitle = function () {
return this.targetDraftTitle;
};
ApiManager.prototype.setTargetDraftTitle = function ( titleObj ) {
this.targetDraftTitle = titleObj;
};
ApiManager.prototype.addPortletLink = function () {
return mw.util.addPortletLink(
this.getPreference( 'menulocation' ),
'#',
mw.message( 'dfy-portlet-label' ).plain(),
'ca-draftify',
mw.message( 'dfy-portlet-tooltip' ).plain(),
null,
this.getPreference( 'menuposition' )
);
};
ApiManager.prototype.getNotificationDate = function () {
// We cache the result of this method so that we will always get the
// same date string as we used for the talk notification, even if a
// user happened to have the page open over a month boundary.
var date, month;
if ( !this.notificationDate ) {
date = new Date();
month = config.months[ date.getMonth() ];
this.notificationDate = mw.message(
'dfy-talk-heading',
month,
date.getFullYear()
).text();
}
return this.notificationDate;
};
ApiManager.prototype.getUserTalkTitle = function () {
return this.userTalkTitle;
};
ApiManager.prototype.openTalkPage = function () {
window.open(
this.getUserTalkTitle().getUrl() +
'#' +
mw.util.wikiUrlencode( this.getNotificationDate() ),
'_top'
);
};
ApiManager.prototype.getPageContent = function ( options ) {
options = options || {};
var self = this;
return $.Deferred( function ( deferred ) {
self.api.get( {
format: 'json',
action: 'query',
prop: 'revisions',
rvprop: 'content',
indexpageids: '',
titles: options.title,
redirects: ''
} ).then( function ( obj ) {
var newTitle, content,
pageId = obj.query.pageids[ 0 ];
// If we got a redirect from the GET request, follow it.
if ( obj.query.redirects ) {
newTitle = obj.query.redirects[ 0 ].to;
} else {
newTitle = options.title;
}
// Get the page content.
if ( pageId === '-1' ) {
if ( options.allowNonexistent ) {
content = '';
} else {
// We got a non-existent page where we weren't
// expecting one, so bail.
return deferred.reject( 'dfy-nonexistent-page-error', { 'error': {
'id': 'dfy-nonexistent-page-error',
'info': mw.message(
'dfy-nonexistent-page-error',
newTitle
).text()
} } );
}
} else {
content = obj.query.pages[ pageId ].revisions[ 0 ][ '*' ];
}
return deferred.resolve( {
title: newTitle,
content: content
} );
} );
} ).promise();
};
ApiManager.prototype.editPage = function ( options ) {
options = options || {};
return this.api.postWithEditToken( {
format: 'json',
action: 'edit',
title: options.title,
summary: options.editSummary + ' ' + mw.message( 'dfy-summary-suffix' ).plain(),
notminor: '',
text: options.content,
watchlist: options.watch ? 'watch' : 'unwatch'
} );
};
ApiManager.prototype.moveDraft = function ( options ) {
options = options || {};
var self = this,
targetTitle = this.getTargetDraftTitle();
if ( !targetTitle ) {
throw new Error( 'moveDraft was called but no target title object was available' );
}
// Get the page content for tagging before we move the page, otherwise
// we risk trying to get the content from the draft page before it is
// moved, resulting in page blanking.
return self.getPageContent( {
title: self.currentDraftTitle.getPrefixedText(),
allowNonexistent: false
} ).then( function ( contentObj ) {
var apiArgs = {
action: 'move',
format: 'json',
from: self.getCurrentDraftTitle().getPrefixedText(),
to: targetTitle.getPrefixedText(),
reason: mw.message( 'dfy-move-summary' ).plain() +
' ' +
mw.message( 'dfy-summary-suffix' ).plain(),
watchlist: options.watchdraft ? 'watch' : 'unwatch'
};
if ( !options.redirect ) {
apiArgs.noredirect = 1;
}
return self.api.postWithEditToken( apiArgs ).then( function () {
return contentObj;
} );
} );
};
ApiManager.prototype.tagDraft = function ( options, contentObj ) {
if ( !contentObj ) {
throw new Error( 'tagDraft was called without contentObj' );
}
var content = contentObj.content,
tag = mw.message( 'dfy-tag-template', this.getTargetUser() ).plain();
// Transform the content
if ( /^\s*{{/.test( content ) ) {
// The page starts with a template.
content = tag + '\n' + content;
} else {
content = tag + '\n\n' + content;
}
return this.editPage( {
title: this.getTargetDraftTitle().getPrefixedText(),
editSummary: mw.message( 'dfy-tag-summary', this.getTargetUser() ).plain(),
content: content,
watch: options.watchdraft
} );
};
ApiManager.prototype.softBlockUser = function ( options ) {
return this.api.postWithEditToken( {
format: 'json',
action: 'block',
user: this.apiManager.getTargetUser(),
expiry: 'infinite',
reason: mw.message( 'dfy-softblock-summary' ).plain(),
allowusertalk: 1
} );
};
ApiManager.prototype.notifyUser = function ( options ) {
options = options || {};
var summary,
self = this;
if ( options.softblock ) {
summary = mw.message( 'dfy-notify-summary-softblock' ).plain();
} else if ( options.suggestrename ) {
summary = mw.message( 'dfy-notify-summary-suggestrename' ).plain();
} else {
summary = mw.message( 'dfy-notify-summary-moveonly' ).plain();
}
return self.getPageContent( {
title: self.getUserTalkTitle().getPrefixedText(),
allowNonexistent: true
} ).then( function ( contentObj ) {
// Generate the new content.
// First, add a heading for the current month if it is not already
// the last heading on the page.
var lastHeading, headings,
content = contentObj.content,
notificationDate = self.getNotificationDate();
if ( /\S/.test( content ) ) {
// Separate the notice from whatever came before.
content += '\n\n';
}
// Add a heading for the current month if there isn't one already.
headings = content.match( /^==[^=].*==[ \t]*$/gm );
if ( headings ) {
lastHeading = headings[ headings.length - 1 ].slice( 2, -2 ).trim();
}
if ( !lastHeading || lastHeading !== notificationDate ) {
content += '== ' + notificationDate + ' ==\n\n';
}
// Add the notification templates.
content += mw.message(
'dfy-notify-template',
self.getTargetDraftTitle() ? self.getTargetDraftTitle().getPrefixedText() : '',
self.getCurrentDraftTitle().getPrefixedText()
).plain();
if ( options.softblock ) {
content += '\n\n' + mw.message( 'dfy-softblock-template' ).plain();
} else if ( options.suggestrename ) {
content += '\n\n' + mw.message( 'dfy-suggestrename-template' ).plain();
}
return self.editPage( {
title: contentObj.title,
editSummary: summary,
content: content,
watch: options.watchusertalk
} );
} );
};
ApiManager.prototype.submit = function ( options ) {
var i, len,
self = this,
actions = [],
promises = {},
whenArgs = [];
// Make handler function for each promise that resolves successfully.
function makeDoneHandler( i ) {
return function ( obj ) {
return self[ actions[ i ].method ]( options, obj );
};
}
try {
this.setTargetDraftTitle( new mw.Title(
mw.message( 'dfy-draft-prefix' ).text() + options.title
) );
// Specify the order in which the actions should be performed.
actions.push( { action: 'move', method: 'moveDraft' } );
actions.push( { action: 'tag', method: 'tagDraft' } );
if ( options.softblock ) {
actions.push( { action: 'softblock', method: 'softBlockUser' } );
}
if ( options.notify ) {
actions.push( { action: 'notify', method: 'notifyUser' } );
}
// Make a chain of promises to perform the actions, and reference the
// promises in the promises object.
promises[ actions[ 0 ].action ] = self[ actions[ 0 ].method ]( options );
for ( i = 1, len = actions.length; i < len; i++ ) {
promises[ actions[ i ].action ] = promises[ actions[ i - 1 ].action ].then(
makeDoneHandler( i )
);
}
} catch ( e ) {
// Create a failed promise with the error info so that users will
// see the error message when they click submit.
promises.move = $.Deferred().reject( 'dfy-submit-error', { 'error': {
'id': 'dfy-submit-error',
'info': e.message || mw.message( 'dfy-progress-unknown-error' ).text()
} } ).promise();
}
// Create an "all" promise that tracks the overall status of the
// submission process.
$.each( promises, function ( key, promise ) {
whenArgs.push( promise );
} );
promises.all = $.when.apply( this, whenArgs );
return promises;
};
/**************************************************************************
* Dialog class
**************************************************************************/
Dialog = function ( options ) {
options = options || {};
this.apiManager = new ApiManager();
this.isLoaded = false;
this.hasBeenSubmitted = false;
Dialog.super.call( this, options );
};
OO.inheritClass( Dialog, OO.ui.ProcessDialog );
Dialog.static.name = 'DraftifyDialog';
Dialog.static.title = 'Draftify';
Dialog.static.actions = [
{ action: 'submit', label: 'Submit', flags: [ 'primary', 'constructive' ] },
{ label: 'Cancel', flags: 'safe' }
];
Dialog.prototype.getApiManager = function () {
return this.apiManager;
};
Dialog.prototype.getBodyHeight = function () {
return 300;
};
Dialog.prototype.initialize = function () {
var apiManager = this.getApiManager();
Dialog.super.prototype.initialize.apply( this, arguments );
// Initialize edit panel
this.editPanel = new OO.ui.PanelLayout( {
expanded: false
} );
this.editFieldset = new OO.ui.FieldsetLayout( {
classes: [ 'container' ]
} );
this.editPanel.$element.append( this.editFieldset.$element );
// Initialize title text input widget
this.titleInput = new OO.ui.TextInputWidget( {
// TODO: Fix the label mess in OOjs-ui so that this can be
// uncommented.
// label: mw.message( 'dfy-draft-prefix' ).plain(),
// labelPosition: 'before',
validate: apiManager.getTitleValidationRegex(),
value: apiManager.getCurrentSubpage()
} );
// Initialize checkbox widgets
this.redirectCheckbox = new OO.ui.CheckboxInputWidget();
this.redirectCheckbox.setSelected( true );
this.redirectCheckbox.setDisabled( true );
this.notifyCheckbox = new OO.ui.CheckboxInputWidget( {
selected: apiManager.getPreference( 'notify' )
} );
this.suggestrenameCheckbox = new OO.ui.CheckboxInputWidget( {
selected: apiManager.getPreference( 'suggestrename' )
} );
this.softblockCheckbox = new OO.ui.CheckboxInputWidget();
this.softblockCheckbox.setDisabled( true );
this.watchUserTalkCheckbox = new OO.ui.CheckboxInputWidget( {
selected: apiManager.getPreference( 'watchusertalk' )
} );
this.watchDraftCheckbox = new OO.ui.CheckboxInputWidget( {
selected: apiManager.getPreference( 'watchdraft' )
} );
// Add widgets to the edit fieldset
this.editFieldset.addItems( [
new OO.ui.FieldLayout( this.titleInput, {
label: mw.message( 'dfy-title-input-label' ).plain()
} ),
new OO.ui.FieldLayout( this.redirectCheckbox, {
label: mw.message( 'dfy-redirect-checkbox-label' ).plain(),
align: 'inline'
} ),
new OO.ui.FieldLayout( this.notifyCheckbox, {
// TODO: Use {{GENDER}} with the relevant username. I had a go
// at this, but apparently jqueryMsg doesn't like me.
label: mw.message( 'dfy-notify-checkbox-label' ).plain(),
align: 'inline'
} ),
new OO.ui.FieldLayout( this.suggestrenameCheckbox, {
// TODO: Use {{GENDER}}
label: mw.message( 'dfy-suggestrename-checkbox-label' ).plain(),
align: 'inline'
} ),
new OO.ui.FieldLayout( this.softblockCheckbox, {
// TODO: Use {{GENDER}}
label: mw.message( 'dfy-softblock-checkbox-label' ).plain(),
align: 'inline'
} ),
new OO.ui.FieldLayout( this.watchUserTalkCheckbox, {
label: mw.message( 'dfy-watch-user-talk-checkbox-label' ).plain(),
align: 'inline'
} ),
new OO.ui.FieldLayout( this.watchDraftCheckbox, {
label: mw.message( 'dfy-watch-draft-checkbox-label' ).plain(),
align: 'inline'
} )
] );
// Initialize submit panel. The progress fields aren't added to the
// fieldset yet - they will be added dynamically when the user submits
// the form.
this.submitPanel = new OO.ui.PanelLayout( {
$: this.$,
expanded: false
} );
this.submitFieldset = new OO.ui.FieldsetLayout( {
classes: [ 'container' ]
} );
this.submitPanel.$element.append( this.submitFieldset.$element );
this.moveProgressLabel = new OO.ui.LabelWidget();
this.moveProgressField = new OO.ui.FieldLayout( this.moveProgressLabel );
this.tagProgressLabel = new OO.ui.LabelWidget();
this.tagProgressField = new OO.ui.FieldLayout( this.tagProgressLabel );
this.softblockProgressLabel = new OO.ui.LabelWidget();
this.softblockProgressField = new OO.ui.FieldLayout( this.softblockProgressLabel );
this.notifyProgressLabel = new OO.ui.LabelWidget();
this.notifyProgressField = new OO.ui.FieldLayout( this.notifyProgressLabel );
this.openTalkProgressField = new OO.ui.FieldLayout( new OO.ui.LabelWidget() );
// Initialize stack widget
this.stackLayout= new OO.ui.StackLayout( {
items: [ this.editPanel, this.submitPanel ],
padded: true
} );
// Add widgets to the DOM
this.$body.append( this.stackLayout.$element );
};
Dialog.prototype.getReadyProcess = function ( data ) {
data = data || {};
var dialog = this;
return Dialog.super.prototype.getReadyProcess.call( this, data )
.next( function () {
if ( !dialog.isLoaded ) {
dialog.pushPending();
dialog.actions.setAbilities( { submit: false } );
dialog.getApiManager().setRights()
.done( function () {
dialog.onLoad();
dialog.actions.setAbilities( { submit: true } );
dialog.popPending();
} )
.fail( function () {
dialog.popPending();
// TODO: use the pretty OOjs-UI error formatting.
alert( 'There was a problem fetching your user rights from the API.' );
/*
return [ new OO.ui.Error(
'There was a problem fetching your user rights from the API',
{ recoverable: false }
) ];
*/
} );
}
if ( dialog.hasBeenSubmitted ) {
// The dialog has already been submitted when it was previously
// opened, so disable the Submit button.
dialog.actions.setAbilities( { submit: false } );
} else {
// XXX: Workaround for text bunching issue in the title input.
// Remove once this is fixed in OOjs-UI.
this.titleInput.focus();
// Make the cursor appear after the value instead of before.
var val = this.titleInput.getValue();
this.titleInput.setValue( '' );
this.titleInput.setValue( val );
}
}, this );
};
Dialog.prototype.onLoad = function () {
var apiManager = this.getApiManager();
// Set checkbox statuses
if ( apiManager.currentUserCan( 'suppressredirect' ) ) {
this.redirectCheckbox.setSelected(
apiManager.getPreference( 'redirect' )
);
this.redirectCheckbox.setDisabled( false );
} else {
this.redirectCheckbox.setSelected( true );
this.redirectCheckbox.setDisabled( true );
}
if ( apiManager.currentUserCan( 'softblock' ) ) {
this.softblockCheckbox.setSelected(
apiManager.getPreference( 'softblock' )
);
this.redirectCheckbox.setDisabled( false );
} else {
this.softblockCheckbox.setSelected( false );
this.softblockCheckbox.setDisabled( true );
}
// Run the event handlers once in case anyone has set strange
// preferences.
this.onSuggestrenameChange();
if ( apiManager.currentUserCan( 'softblock' ) ) {
this.onSoftblockChange();
}
this.onNotifyChange();
this.onTitleChange();
// Add event handlers
this.softblockCheckbox.on( 'change', this.onSoftblockChange, null, this );
this.suggestrenameCheckbox.on( 'change', this.onSuggestrenameChange, null, this );
this.notifyCheckbox.on( 'change', this.onNotifyChange, null, this );
this.titleInput.on( 'change', this.onTitleChange, null, this );
// Mark as loaded
this.isLoaded = true;
};
Dialog.prototype.onSuggestrenameChange = function () {
if ( this.suggestrenameCheckbox.isSelected() ) {
this.notifyCheckbox.setSelected( true );
this.notifyCheckbox.setDisabled( true );
this.softblockCheckbox.setSelected( false );
this.softblockCheckbox.setDisabled( true );
} else {
if ( this.getApiManager().currentUserCan( 'softblock' ) ) {
this.softblockCheckbox.setDisabled( false );
}
this.notifyCheckbox.setDisabled( false );
}
this.onNotifyChange();
};
Dialog.prototype.onSoftblockChange = function () {
if ( this.softblockCheckbox.isSelected() ) {
this.notifyCheckbox.setSelected( true );
this.notifyCheckbox.setDisabled( true );
this.suggestrenameCheckbox.setSelected( false );
this.suggestrenameCheckbox.setDisabled( true );
} else {
this.suggestrenameCheckbox.setDisabled( false );
this.notifyCheckbox.setDisabled( false );
}
this.onNotifyChange();
};
Dialog.prototype.onNotifyChange = function () {
if ( this.notifyCheckbox.isSelected() ) {
this.watchUserTalkCheckbox.setDisabled( false );
} else {
this.watchUserTalkCheckbox.setSelected( false );
this.watchUserTalkCheckbox.setDisabled( true );
}
};
Dialog.prototype.onTitleChange = function () {
var self = this,
promise = this.titleInput.getValidity();
promise.done( function () {
self.actions.setAbilities( { submit: true } );
} );
promise.fail( function () {
self.actions.setAbilities( { submit: false } );
} );
};
Dialog.prototype.onSubmit = function () {
var options, promises, messages,
self = this,
apiManager = this.getApiManager(),
targetUser = apiManager.getTargetUser();
// Record that the dialog has been submitted. This is necessary to
// prevent the "Submit" button from being reactivated after the window
// is closed and reopened.
self.hasBeenSubmitted = true;
// Disable input
self.actions.setAbilities( { submit: false } );
options = {
title: this.titleInput.getValue(),
redirect: this.redirectCheckbox.isSelected(),
notify: this.notifyCheckbox.isSelected(),
suggestrename: this.suggestrenameCheckbox.isSelected(),
softblock: this.softblockCheckbox.isSelected(),
watchusertalk: this.watchUserTalkCheckbox.isSelected(),
watchdraft: this.watchDraftCheckbox.isSelected()
};
promises = apiManager.submit( options );
// Increase the pending level for each promise we have.
$.each( promises, function ( key, value ) {
if ( value ) {
self.pushPending();
}
} );
// Set the progress labels, hide them from view, and add them to the
// fieldset.
function addField( field, label, promise ) {
if ( !promise ) {
return;
}
field
.setLabel( label )
.setData( promise )
.toggle();
self.submitFieldset.addItems( [ field ] );
}
addField(
self.moveProgressField,
mw.message( 'dfy-move-progress-label' ).text(),
promises.move
);
addField(
self.tagProgressField,
mw.message( 'dfy-tag-progress-label' ).text(),
promises.tag
);
addField(
self.softblockProgressField,
mw.message( 'dfy-softblock-progress-label', targetUser ).text(),
promises.softblock
);
addField(
self.notifyProgressField,
mw.message( 'dfy-notify-progress-label', targetUser ).text(),
promises.notify
);
self.openTalkProgressField
.setLabel( mw.message( 'dfy-open-talk-progress-label' ).text() )
.toggle();
self.submitFieldset.addItems( [ self.openTalkProgressField ] );
// Update progress
// This involves making the process fields visible, adding the progress
// labels on success or failure, and reducing the pending level.
self.moveProgressField.toggle();
$.each( self.submitFieldset.getItems(), function ( i, field ) {
var
promise = field.getData(),
label = field.getField();
if ( !promise ) {
return;
}
promise
.done( function () {
var nextField;
label.setLabel( $( '<span>' )
.addClass( 'draftify-success' )
.text( mw.message( 'dfy-progress-success' ).text() )
);
// Only display the next field on success, as on error they
// would all have the same error messages.
nextField = self.submitFieldset.getItems()[ i + 1 ];
if ( nextField ) {
nextField.toggle();
}
} )
.fail( function ( id, obj ) {
if ( obj && obj.error && obj.error.info ) {
label.setLabel( $( '<span>' )
.addClass( 'draftify-error' )
.text( mw.message(
'dfy-progress-failed',
obj.error.info
).text() )
);
} else {
label.setLabel(
mw.message( 'dfy-progress-unknown-error' ).text()
);
}
} )
.always( function () {
self.popPending();
} );
} );
// Set final actions
promises.all.done( function () {
apiManager.openTalkPage();
} );
promises.all.fail( function () {
// Pop pending only on failure, so that it still looks like we're
// loading something while the talk page is being opened.
self.popPending();
} );
// Switch to the panel view so that users can see what's going on.
self.stackLayout.setItem( self.submitPanel );
};
Dialog.prototype.getActionProcess = function ( action ) {
return Dialog.super.prototype.getActionProcess.call( this, action )
.next( function () {
if ( action === 'submit' ) {
return this.onSubmit();
} else {
return Dialog.super.prototype.getActionProcess.call( this, action );
}
}, this );
};
/**************************************************************************
* Main
**************************************************************************/
function main() {
// Load CSS
importStylesheet( "User:Mr. Stradivarius/gadgets/Draftify.css" );
// Set up objects
var portletLink, windowManager,
draftifyDialog = new Dialog( { size: 'medium' } ),
apiManager = draftifyDialog.getApiManager();
// Set up window manager
windowManager = new OO.ui.WindowManager();
$( 'body' ).append( windowManager.$element );
windowManager.addWindows( [ draftifyDialog ] );
// Add portlet link
portletLink = apiManager.addPortletLink();
$( portletLink ).click( function ( event ) {
event.preventDefault();
windowManager.openWindow( draftifyDialog );
} );
}
main();
} );
// </nowiki>