User:Mr. Stradivarius/gadgets/Draftify.js

//

/*

* 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 Draft namespace is the preferred location for 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}} ',

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

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

.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();

} );

//