/*! * VisualEditor UserInterface MWSaveDialog class. * * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * Dialog for saving MediaWiki pages. * * Note that most methods are not safe to call before the dialog has initialized, except where * noted otherwise. * * @class * @extends ve.ui.FragmentDialog * * @constructor * @param {Object} [config] Config options */ ve.ui.MWSaveDialog = function VeUiMwSaveDialog( config ) { // Parent constructor ve.ui.MWSaveDialog.super.call( this, config ); // Properties this.editSummaryByteLimit = 255; this.restoring = false; this.messages = {}; this.setupDeferred = $.Deferred(); this.target = null; this.checkboxesByName = null; // Initialization this.$element.addClass( 've-ui-mwSaveDialog' ); }; /* Inheritance */ OO.inheritClass( ve.ui.MWSaveDialog, ve.ui.FragmentDialog ); /* Static Properties */ ve.ui.MWSaveDialog.static.name = 'mwSave'; ve.ui.MWSaveDialog.static.title = OO.ui.deferMsg( 'visualeditor-savedialog-title-save' ); ve.ui.MWSaveDialog.static.actions = [ { action: 'save', label: OO.ui.deferMsg( 'visualeditor-savedialog-label-save' ), flags: [ 'primary', 'constructive' ], modes: [ 'save', 'review' ], accessKey: 's' }, { label: OO.ui.deferMsg( 'visualeditor-savedialog-label-resume-editing' ), flags: [ 'safe', 'back' ], modes: [ 'save', 'review', 'conflict' ] }, { action: 'review', label: OO.ui.deferMsg( 'visualeditor-savedialog-label-review' ), modes: 'save' }, { action: 'approve', label: OO.ui.deferMsg( 'visualeditor-savedialog-label-review-good' ), modes: 'review' }, { action: 'resolve', label: OO.ui.deferMsg( 'visualeditor-savedialog-label-resolve-conflict' ), flags: [ 'primary', 'constructive' ], modes: 'conflict' } ]; /* Events */ /** * @event save * @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save * succeeds/fails. * Emitted when the user clicks the save button */ /** * @event review * Emitted when the user clicks the review changes button */ /** * @event resolve * Emitted when the user clicks the resolve conflict button */ /** * @event retry * Emitted when the user clicks the retry/continue save button after an error. */ /* Methods */ /** * Set review content and show review panel. * * @param {string} content Diff HTML or wikitext */ ve.ui.MWSaveDialog.prototype.setDiffAndReview = function ( content ) { this.$reviewViewer.empty().append( content ); this.actions.setAbilities( { approve: true } ); this.popPending(); this.swapPanel( 'review' ); }; /** * @inheritdoc */ ve.ui.MWSaveDialog.prototype.pushPending = function () { this.getActions().setAbilities( { review: false } ); return ve.ui.MWSaveDialog.super.prototype.pushPending.call( this ); }; /** * @inheritdoc */ ve.ui.MWSaveDialog.prototype.popPending = function () { var ret = ve.ui.MWSaveDialog.super.prototype.popPending.call( this ); if ( !this.isPending() ) { this.getActions().setAbilities( { review: true } ); } return ret; }; /** * Clear the diff displayed in the review panel, if any. */ ve.ui.MWSaveDialog.prototype.clearDiff = function () { this.$reviewViewer.empty(); }; /** * Swap state in the save dialog. * * @param {string} panel One of 'save', 'review', 'conflict' or 'nochanges' * @return {jQuery} The now active panel * @throws {Error} Unknown saveDialog panel */ ve.ui.MWSaveDialog.prototype.swapPanel = function ( panel ) { var currentEditSummaryWikitext, size = 'medium', dialog = this, panelObj = dialog[ panel + 'Panel' ]; if ( ( [ 'save', 'review', 'conflict', 'nochanges' ].indexOf( panel ) ) === -1 ) { throw new Error( 'Unknown saveDialog panel: ' + panel ); } // Update the window title // The following messages can be used here: // visualeditor-savedialog-title-save // visualeditor-savedialog-title-reviews // visualeditor-savedialog-title-conflict // visualeditor-savedialog-title-nochanges this.title.setLabel( ve.msg( 'visualeditor-savedialog-title-' + panel ) ); // Reset save button if we disabled it for e.g. unrecoverable spam error this.actions.setAbilities( { save: true } ); // On panels without inputs, ensure the dialog is focused so events // are captured, e.g. 'Esc' to close this.$content[ 0 ].focus(); switch ( panel ) { case 'save': this.actions.setMode( 'save' ); // HACK: FF needs *another* defer setTimeout( function () { dialog.editSummaryInput.moveCursorToEnd(); } ); break; case 'conflict': this.actions .setAbilities( { save: false } ) .setMode( 'conflict' ); break; case 'review': size = 'larger'; currentEditSummaryWikitext = this.editSummaryInput.getValue(); if ( this.lastEditSummaryWikitext === undefined || this.lastEditSummaryWikitext !== currentEditSummaryWikitext ) { if ( this.editSummaryXhr ) { this.editSummaryXhr.abort(); } this.lastEditSummaryWikitext = currentEditSummaryWikitext; this.$reviewEditSummary.empty(); if ( !currentEditSummaryWikitext || currentEditSummaryWikitext.trim() === '' ) { // Don't bother with an API request for an empty summary this.$reviewEditSummary.parent().addClass( 'oo-ui-element-hidden' ); } else { this.$reviewEditSummary.parent() .removeClass( 'oo-ui-element-hidden' ) .addClass( 'mw-ajax-loader' ); this.editSummaryXhr = new mw.Api().post( { action: 'parse', summary: currentEditSummaryWikitext } ).done( function ( result ) { if ( result.parse.parsedsummary[ '*' ] === '' ) { dialog.$reviewEditSummary.parent().addClass( 'oo-ui-element-hidden' ); } else { dialog.$reviewEditSummary.html( ve.msg( 'parentheses', result.parse.parsedsummary[ '*' ] ) ); } } ).fail( function () { dialog.$reviewEditSummary.parent().addClass( 'oo-ui-element-hidden' ); } ).always( function () { dialog.$reviewEditSummary.parent().removeClass( 'mw-ajax-loader' ); dialog.updateSize(); } ); } } /* falls through */ case 'nochanges': this.actions.setMode( 'review' ); break; } // Show the target panel this.panels.setItem( panelObj ); this.setSize( size ); mw.hook( 've.saveDialog.stateChanged' ).fire(); return dialog; }; /** * Show a message in the save dialog. * * @param {string} name Message's unique name * @param {string|jQuery|Array} message Message content (string of HTML, jQuery object or array of * Node objects) * @param {Object} [options] * @param {boolean} [options.wrap="warning"] Whether to wrap the message in a paragraph and if * so, how. One of "warning", "error" or false. */ ve.ui.MWSaveDialog.prototype.showMessage = function ( name, message, options ) { var $message; if ( !this.messages[ name ] ) { options = options || {}; if ( options.wrap === undefined ) { options.wrap = 'warning'; } $message = $( '
' ); if ( options.wrap !== false ) { $message.append( $( '' ).append(
// visualeditor-savedialog-label-error
// visualeditor-savedialog-label-warning
$( '' ).text( mw.msg( 'visualeditor-savedialog-label-' + options.wrap ) ),
document.createTextNode( mw.msg( 'colon-separator' ) ),
message
) );
} else {
$message.append( message );
}
this.$saveMessages.append( $message );
this.messages[ name ] = $message;
}
};
/**
* Remove a message from the save dialog.
*
* @param {string} name Message's unique name
*/
ve.ui.MWSaveDialog.prototype.clearMessage = function ( name ) {
if ( this.messages[ name ] ) {
this.messages[ name ].remove();
delete this.messages[ name ];
}
};
/**
* Remove all messages from the save dialog.
*/
ve.ui.MWSaveDialog.prototype.clearAllMessages = function () {
this.$saveMessages.empty();
this.messages = {};
};
/**
* Reset the fields of the save dialog.
*
* @method
*/
ve.ui.MWSaveDialog.prototype.reset = function () {
// Reset summary input
this.editSummaryInput.setValue( '' );
// Uncheck minoredit
if ( this.checkboxesByName.wpMinoredit ) {
this.checkboxesByName.wpMinoredit.setSelected( false );
}
// Clear the diff
this.$reviewViewer.empty();
};
/**
* Initialize MediaWiki page specific checkboxes.
*
* This method is safe to call even when the dialog hasn't been initialized yet.
*
* @param {OO.ui.FieldLayout[]} checkboxFields Checkbox fields
*/
ve.ui.MWSaveDialog.prototype.setupCheckboxes = function ( checkboxFields ) {
var dialog = this;
this.setupDeferred.done( function () {
checkboxFields.forEach( function ( field ) {
dialog.$saveCheckboxes.append( field.$element );
} );
} );
};
/**
* Change the edit summary prefilled in the save dialog.
*
* This method is safe to call even when the dialog hasn't been initialized yet.
*
* @param {string} summary Edit summary to prefill
*/
ve.ui.MWSaveDialog.prototype.setEditSummary = function ( summary ) {
var dialog = this;
this.setupDeferred.done( function () {
dialog.editSummaryInput.setValue( summary );
} );
};
/**
* @inheritdoc
*/
ve.ui.MWSaveDialog.prototype.initialize = function () {
var saveAccessKey,
dialog = this;
// Parent method
ve.ui.MWSaveDialog.super.prototype.initialize.call( this );
// Properties
this.panels = new OO.ui.StackLayout( { scrollable: true } );
this.savePanel = new OO.ui.PanelLayout( {
expanded: false,
scrollable: true,
padded: true,
classes: [ 've-ui-mwSaveDialog-savePanel' ]
} );
// Byte counter in edit summary
this.editSummaryCountLabel = new OO.ui.LabelWidget( {
classes: [ 've-ui-mwSaveDialog-editSummary-count' ],
label: String( this.editSummaryByteLimit ),
title: ve.msg( 'visualeditor-editsummary-bytes-remaining' )
} );
// Save panel
this.$editSummaryLabel = $( ' ' ).addClass( 've-ui-mwSaveDialog-license' )
.html( ve.init.platform.getParsedMessage( 'copyrightwarning' ) )
.find( 'a' ).attr( 'target', '_blank' ).end()
);
this.savePanel.$element.append(
this.$editSummaryLabel,
this.editSummaryInput.$element,
this.$saveOptions,
this.$saveMessages,
this.$saveActions,
this.$saveFoot
);
// Review panel
this.reviewPanel = new OO.ui.PanelLayout( {
expanded: false,
scrollable: true,
padded: true
} );
this.$reviewViewer = $( '
' ), this.$reviewEditSummary ),
this.$reviewViewer,
this.$reviewActions
);
// Conflict panel
this.conflictPanel = new OO.ui.PanelLayout( {
expanded: false,
scrollable: true,
padded: true
} );
this.$conflict = $( '