Merge "[BREAKING CHANGE] Target*: Replace target events with methods"

This commit is contained in:
jenkins-bot 2015-08-04 20:35:35 +00:00 committed by Gerrit Code Review
commit d103896e4a
3 changed files with 309 additions and 312 deletions

View file

@ -8,7 +8,7 @@
/*global confirm, alert */ /*global confirm, alert */
/** /**
* Initialization MediaWiki view page target. * MediaWiki desktop article target.
* *
* @class * @class
* @extends ve.init.mw.Target * @extends ve.init.mw.Target
@ -44,6 +44,8 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config
this.toolbarSetupDeferred = null; this.toolbarSetupDeferred = null;
this.welcomeDialog = null; this.welcomeDialog = null;
this.welcomeDialogPromise = null; this.welcomeDialogPromise = null;
this.captcha = null;
this.docToSave = null;
// If this is true then #transformPage / #restorePage will not call pushState // If this is true then #transformPage / #restorePage will not call pushState
// This is to avoid adding a new history entry for the url we just got from onpopstate // This is to avoid adding a new history entry for the url we just got from onpopstate
@ -66,24 +68,6 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config
this.originalDocumentTitle = document.title; this.originalDocumentTitle = document.title;
this.tabLayout = mw.config.get( 'wgVisualEditorConfig' ).tabLayout; this.tabLayout = mw.config.get( 'wgVisualEditorConfig' ).tabLayout;
// Events
this.connect( this, {
save: 'onSave',
saveErrorEmpty: 'onSaveErrorEmpty',
saveErrorSpamBlacklist: 'onSaveErrorSpamBlacklist',
saveErrorAbuseFilter: 'onSaveErrorAbuseFilter',
saveErrorNewUser: 'onSaveErrorNewUser',
saveErrorCaptcha: 'onSaveErrorCaptcha',
saveErrorUnknown: 'onSaveErrorUnknown',
saveErrorPageDeleted: 'onSaveErrorPageDeleted',
saveErrorTitleBlacklist: 'onSaveErrorTitleBlacklist',
editConflict: 'onEditConflict',
showChanges: 'onShowChanges',
showChangesError: 'onShowChangesError',
noChanges: 'onNoChanges',
serializeError: 'onSerializeError'
} );
// Initialization // Initialization
this.$element.addClass( 've-init-mw-desktopArticleTarget' ); this.$element.addClass( 've-init-mw-desktopArticleTarget' );
@ -457,15 +441,11 @@ ve.init.mw.DesktopArticleTarget.prototype.cancel = function ( trackMechanism ) {
}; };
/** /**
* Handle failed DOM load event. * @inheritdoc
*
* @method
* @param {string} errorTypeText
* @param {string} error
*/ */
ve.init.mw.DesktopArticleTarget.prototype.loadFail = function ( errorText, error ) { ve.init.mw.DesktopArticleTarget.prototype.loadFail = function ( errorText, error ) {
// Parent method // Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.loadFail.call( this, errorText, error ); ve.init.mw.DesktopArticleTarget.super.prototype.loadFail.apply( this, arguments );
// Don't show an error if the load was manually aborted // Don't show an error if the load was manually aborted
// The response.status check here is to catch aborts triggered by navigation away from the page // The response.status check here is to catch aborts triggered by navigation away from the page
@ -507,9 +487,7 @@ ve.init.mw.DesktopArticleTarget.prototype.loadFail = function ( errorText, error
}; };
/** /**
* Once surface is ready ready, init UI * @inheritdoc
*
* @method
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onSurfaceReady = function () { ve.init.mw.DesktopArticleTarget.prototype.onSurfaceReady = function () {
var surfaceReadyTime = ve.now(), var surfaceReadyTime = ve.now(),
@ -590,20 +568,14 @@ ve.init.mw.DesktopArticleTarget.prototype.onViewTabClick = function ( e ) {
}; };
/** /**
* Handle successful DOM save event. * @inheritdoc
*
* @method
* @param {string} html Rendered page HTML from server
* @param {string} categoriesHtml Rendered categories HTML from server
* @param {number} newid New revision id, undefined if unchanged
* @param {boolean} isRedirect Whether this page is a redirect or not
* @param {string} displayTitle What HTML to show as the page title
* @param {Object} lastModified Object containing user-formatted date
and time strings, or undefined if we made no change.
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onSave = function ( ve.init.mw.DesktopArticleTarget.prototype.saveComplete = function (
html, categoriesHtml, newid, isRedirect, displayTitle, lastModified, contentSub html, categoriesHtml, newid, isRedirect, displayTitle, lastModified, contentSub
) { ) {
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.saveComplete.apply( this, arguments );
var newUrlParams, watchChecked; var newUrlParams, watchChecked;
this.saveDeferred.resolve(); this.saveDeferred.resolve();
if ( !this.pageExists || this.restoring ) { if ( !this.pageExists || this.restoring ) {
@ -676,82 +648,7 @@ ve.init.mw.DesktopArticleTarget.prototype.onSave = function (
/** /**
* @inheritdoc * @inheritdoc
*/ */
ve.init.mw.DesktopArticleTarget.prototype.saveFail = function () { ve.init.mw.DesktopArticleTarget.prototype.saveErrorCaptcha = function ( editApi ) {
this.pageDeletedWarning = false;
ve.init.mw.DesktopArticleTarget.super.prototype.saveFail.apply( this, arguments );
};
/**
* Update save dialog message on general error
*
* @method
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorEmpty = function () {
this.showSaveError( ve.msg( 'visualeditor-saveerror', 'Empty server response' ), false /* prevents reapply */ );
};
/**
* Update save dialog message on spam blacklist error
*
* @method
* @param {Object} editApi
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorSpamBlacklist = function ( editApi ) {
this.showSaveError(
$( $.parseHTML( editApi.sberrorparsed ) ),
false // prevents reapply
);
};
/**
* Update save dialog message on abuse filter error
*
* @method
* @param {Object} editApi
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorAbuseFilter = function ( editApi ) {
this.showSaveError( $( $.parseHTML( editApi.warning ) ) );
// Don't disable the save button. If the action is not disallowed the user may save the
// edit by pressing Save again. The AbuseFilter API currently has no way to distinguish
// between filter triggers that are and aren't disallowing the action.
};
/**
* Update save dialog message on title blacklist error
*
* @method
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorTitleBlacklist = function () {
this.showSaveError( mw.msg( 'visualeditor-saveerror-titleblacklist' ) );
};
/**
* Update save dialog when token fetch indicates another user is logged in
*
* @method
* @param {string|null} username Name of newly logged-in user, or null if anonymous
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorNewUser = function ( username ) {
var badToken, userMsg;
badToken = document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' );
// mediawiki.jqueryMsg has a bug with [[User:$1|$1]] (bug 51388)
if ( username === null ) {
userMsg = 'visualeditor-savedialog-identify-anon';
} else {
userMsg = 'visualeditor-savedialog-identify-user---' + username;
}
this.showSaveError(
$( badToken ).add( $.parseHTML( mw.message( userMsg ).parse() ) )
);
};
/**
* Update save dialog on captcha error
*
* @method
* @param {Object} editApi
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorCaptcha = function ( editApi ) {
var $captchaDiv = $( '<div>' ), var $captchaDiv = $( '<div>' ),
$captchaParagraph = $( '<p>' ); $captchaParagraph = $( '<p>' );
@ -799,38 +696,6 @@ ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorCaptcha = function ( editAp
this.saveDialog.popPending(); this.saveDialog.popPending();
}; };
/**
* Update save dialog message on unknown error
*
* @method
* @param {Object} editApi
* @param {Object|null} data API response data
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorUnknown = function ( editApi, data ) {
this.showSaveError(
$( document.createTextNode(
( editApi && editApi.info ) ||
( data.error && data.error.info ) ||
( editApi && editApi.code ) ||
( data.error && data.error.code ) ||
'Unknown error'
) ),
false // prevents reapply
);
};
/**
* Update save dialog message on page deleted error
*
* @method
*/
ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorPageDeleted = function () {
var continueLabel = mw.msg( 'ooui-dialog-process-continue' );
this.pageDeletedWarning = true;
this.showSaveError( mw.msg( 'visualeditor-recreate', continueLabel ), true, true );
};
/** /**
* Handle MWSaveDialog retry events * Handle MWSaveDialog retry events
* So we can handle trying to save again after page deletion warnings * So we can handle trying to save again after page deletion warnings
@ -843,26 +708,19 @@ ve.init.mw.DesktopArticleTarget.prototype.onSaveRetry = function () {
}; };
/** /**
* Update save dialog api-save-error message * @inheritdoc
*
* @method
* @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of
* Node objects)
* @param {boolean} [allowReapply=true] Whether or not to allow the user to reapply.
* Reset when swapping panels. Assumed to be true unless explicitly set to false.
* @param {boolean} [warning=false] Whether or not this is a warning.
*/ */
ve.init.mw.DesktopArticleTarget.prototype.showSaveError = function ( msg, allowReapply, warning ) { ve.init.mw.DesktopArticleTarget.prototype.showSaveError = function ( msg, allowReapply, warning ) {
this.saveDeferred.reject( [ new OO.ui.Error( msg, { recoverable: allowReapply, warning: warning } ) ] ); this.saveDeferred.reject( [ new OO.ui.Error( msg, { recoverable: allowReapply, warning: warning } ) ] );
}; };
/** /**
* Handle Show changes event. * @inheritdoc
*
* @method
* @param {string} diffHtml
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onShowChanges = function ( diffHtml ) { ve.init.mw.DesktopArticleTarget.prototype.showChangesDiff = function ( diffHtml ) {
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.showChangesDiff.apply( this, arguments );
// Invalidate the viewer diff on next change // Invalidate the viewer diff on next change
this.getSurface().getModel().getDocument().once( 'transact', this.getSurface().getModel().getDocument().once( 'transact',
this.saveDialog.clearDiff.bind( this.saveDialog ) this.saveDialog.clearDiff.bind( this.saveDialog )
@ -871,25 +729,23 @@ ve.init.mw.DesktopArticleTarget.prototype.onShowChanges = function ( diffHtml )
}; };
/** /**
* Handle failed show changes event. * @inheritdoc
*
* @method
* @param {Object} jqXHR
* @param {string} status Text status message
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onShowChangesError = function ( jqXHR, status ) { ve.init.mw.DesktopArticleTarget.prototype.showChangesFail = function ( jqXHR, status ) {
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.showChangesFail.apply( this, arguments );
alert( ve.msg( 'visualeditor-differror', status ) ); alert( ve.msg( 'visualeditor-differror', status ) );
this.saveDialog.popPending(); this.saveDialog.popPending();
}; };
/** /**
* Called if a call to target.serialize() failed. * @inheritdoc
*
* @method
* @param {jqXHR|null} jqXHR
* @param {string} status Text status message
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onSerializeError = function ( jqXHR, status ) { ve.init.mw.DesktopArticleTarget.prototype.serializeFail = function ( jqXHR, status ) {
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.serializeFail.apply( this, arguments );
alert( ve.msg( 'visualeditor-serializeerror', status ) ); alert( ve.msg( 'visualeditor-serializeerror', status ) );
this.getSurface().getDialogs().closeWindow( 'wikitextswitchconfirm' ); this.getSurface().getDialogs().closeWindow( 'wikitextswitchconfirm' );
@ -902,21 +758,17 @@ ve.init.mw.DesktopArticleTarget.prototype.onSerializeError = function ( jqXHR, s
}; };
/** /**
* Handle edit conflict event. * @inheritdoc
*
* @method
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onEditConflict = function () { ve.init.mw.DesktopArticleTarget.prototype.editConflict = function () {
this.saveDialog.popPending(); this.saveDialog.popPending();
this.saveDialog.swapPanel( 'conflict' ); this.saveDialog.swapPanel( 'conflict' );
}; };
/** /**
* Handle failed show changes event. * @inheritdoc
*
* @method
*/ */
ve.init.mw.DesktopArticleTarget.prototype.onNoChanges = function () { ve.init.mw.DesktopArticleTarget.prototype.noChanges = function () {
this.saveDialog.popPending(); this.saveDialog.popPending();
this.saveDialog.swapPanel( 'nochanges' ); this.saveDialog.swapPanel( 'nochanges' );
this.saveDialog.getActions().setAbilities( { approve: true } ); this.saveDialog.getActions().setAbilities( { approve: true } );
@ -944,7 +796,7 @@ ve.init.mw.DesktopArticleTarget.prototype.onSaveDialogReview = function () {
this.saveDialog.getActions().setAbilities( { approve: false } ); this.saveDialog.getActions().setAbilities( { approve: false } );
this.saveDialog.pushPending(); this.saveDialog.pushPending();
if ( this.pageExists ) { if ( this.pageExists ) {
// Has no callback, handled via target.onShowChanges // Has no callback, handled via target.showChangesDiff
this.showChanges( this.docToSave ); this.showChanges( this.docToSave );
} else { } else {
this.serialize( this.docToSave, this.onSaveDialogReviewComplete.bind( this ) ); this.serialize( this.docToSave, this.onSaveDialogReviewComplete.bind( this ) );
@ -969,25 +821,23 @@ ve.init.mw.DesktopArticleTarget.prototype.onSaveDialogReviewComplete = function
}; };
/** /**
* Try to save the current document. * @inheritdoc
* @fires saveInitiated
* @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save * @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save
* succeeds/fails. * succeeds/fails.
*/ */
ve.init.mw.DesktopArticleTarget.prototype.saveDocument = function ( saveDeferred ) { ve.init.mw.DesktopArticleTarget.prototype.startSave = function ( saveDeferred ) {
if ( this.deactivating ) { if ( this.deactivating ) {
return false; return false;
} }
var saveOptions = this.getSaveOptions();
this.emit( 'saveInitiated' );
// Reset any old captcha data // Reset any old captcha data
if ( this.captcha ) { if ( this.captcha ) {
this.saveDialog.clearMessage( 'captcha' ); this.saveDialog.clearMessage( 'captcha' );
delete this.captcha; delete this.captcha;
} }
var saveOptions = this.getSaveOptions();
if ( if (
+mw.user.options.get( 'forceeditsummary' ) && +mw.user.options.get( 'forceeditsummary' ) &&
saveOptions.summary === '' && saveOptions.summary === '' &&
@ -1001,7 +851,8 @@ ve.init.mw.DesktopArticleTarget.prototype.saveDocument = function ( saveDeferred
); );
this.saveDialog.popPending(); this.saveDialog.popPending();
} else { } else {
this.save( this.docToSave, saveOptions ); // Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.startSave.call( this );
this.saveDeferred = saveDeferred; this.saveDeferred = saveDeferred;
} }
}; };
@ -1076,9 +927,7 @@ ve.init.mw.DesktopArticleTarget.prototype.submitWithSaveFields = function ( fiel
}; };
/** /**
* Get edit API options from the save dialog form. * @inheritdoc
*
* @returns {Object} Save options for submission to the MediaWiki API
*/ */
ve.init.mw.DesktopArticleTarget.prototype.getSaveOptions = function () { ve.init.mw.DesktopArticleTarget.prototype.getSaveOptions = function () {
var key, var key,
@ -1197,20 +1046,14 @@ ve.init.mw.DesktopArticleTarget.prototype.attachToolbarSaveButton = function ()
/** /**
* @inheritdoc * @inheritdoc
* @fires saveWorkflowBegin
*/ */
ve.init.mw.DesktopArticleTarget.prototype.showSaveDialog = function () { ve.init.mw.DesktopArticleTarget.prototype.showSaveDialog = function () {
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.showSaveDialog.call( this );
var target = this, var target = this,
windowAction = ve.ui.actionFactory.create( 'window', this.getSurface() ); windowAction = ve.ui.actionFactory.create( 'window', this.getSurface() );
this.emit( 'saveWorkflowBegin' );
// Preload the serialization
if ( !this.docToSave ) {
this.docToSave = this.getSurface().getDom();
}
this.prepareCacheKey( this.docToSave );
// Connect events to save dialog // Connect events to save dialog
this.getSurface().getDialogs().getWindow( 'mwSave' ).done( function ( win ) { this.getSurface().getDialogs().getWindow( 'mwSave' ).done( function ( win ) {
if ( !target.saveDialog ) { if ( !target.saveDialog ) {
@ -1218,7 +1061,7 @@ ve.init.mw.DesktopArticleTarget.prototype.showSaveDialog = function () {
// Connect to save dialog // Connect to save dialog
target.saveDialog.connect( target, { target.saveDialog.connect( target, {
save: 'saveDocument', save: 'startSave',
review: 'onSaveDialogReview', review: 'onSaveDialogReview',
resolve: 'onSaveDialogResolveConflict', resolve: 'onSaveDialogResolveConflict',
retry: 'onSaveRetry', retry: 'onSaveRetry',
@ -1430,10 +1273,10 @@ ve.init.mw.DesktopArticleTarget.prototype.onWindowPopState = function ( e ) {
* @method * @method
* @param {string} html Rendered HTML from server * @param {string} html Rendered HTML from server
* @param {string} categoriesHtml Rendered categories HTML from server * @param {string} categoriesHtml Rendered categories HTML from server
* @param {string} displayTitle What HTML to show as the page title * @param {string} displayTitle HTML to show as the page title
* @param {Object} lastModified Object containing user-formatted date * @param {Object} lastModified Object containing user-formatted date
and time strings, or undefined if we made no change. * and time strings, or undefined if we made no change.
* @param {string} contentSub What HTML to show as the content subtitle * @param {string} contentSub HTML to show as the content subtitle
*/ */
ve.init.mw.DesktopArticleTarget.prototype.replacePageContent = function ( ve.init.mw.DesktopArticleTarget.prototype.replacePageContent = function (
html, categoriesHtml, displayTitle, lastModified, contentSub html, categoriesHtml, displayTitle, lastModified, contentSub

View file

@ -6,12 +6,12 @@
*/ */
/** /**
* MediaWiki mobile article target.
* *
* @class * @class
* @extends ve.init.mw.Target * @extends ve.init.mw.Target
* *
* @constructor * @constructor
* @param {jQuery} $container Container to render target into
* @param {Object} [config] Configuration options * @param {Object} [config] Configuration options
* @cfg {number} [section] Number of the section target should scroll to * @cfg {number} [section] Number of the section target should scroll to
* @cfg {boolean} [isIos=false] Whether the platform is an iOS device * @cfg {boolean} [isIos=false] Whether the platform is an iOS device
@ -37,18 +37,6 @@ ve.init.mw.MobileArticleTarget = function VeInitMwMobileArticleTarget( config )
OO.inheritClass( ve.init.mw.MobileArticleTarget, ve.init.mw.Target ); OO.inheritClass( ve.init.mw.MobileArticleTarget, ve.init.mw.Target );
/* Events */
/**
* @event back
* Leave the editor
*/
/**
* @event editSource
* Switch to edit source mode
*/
/* Static Properties */ /* Static Properties */
ve.init.mw.MobileArticleTarget.static.toolbarGroups = [ ve.init.mw.MobileArticleTarget.static.toolbarGroups = [
@ -84,7 +72,7 @@ ve.init.mw.MobileArticleTarget.static.name = 'mobile';
/* Methods */ /* Methods */
/** /**
* Once surface is ready ready, init UI. * @inheritdoc
*/ */
ve.init.mw.MobileArticleTarget.prototype.onSurfaceReady = function () { ve.init.mw.MobileArticleTarget.prototype.onSurfaceReady = function () {
// Parent method // Parent method
@ -94,12 +82,7 @@ ve.init.mw.MobileArticleTarget.prototype.onSurfaceReady = function () {
}; };
/** /**
* Create a surface. * @inheritdoc
*
* @method
* @param {ve.dm.Document} dmDoc Document model
* @param {Object} [config] Configuration options
* @returns {ve.ui.MobileSurface}
*/ */
ve.init.mw.MobileArticleTarget.prototype.createSurface = function ( dmDoc, config ) { ve.init.mw.MobileArticleTarget.prototype.createSurface = function ( dmDoc, config ) {
return new ve.ui.MobileSurface( dmDoc, config ); return new ve.ui.MobileSurface( dmDoc, config );
@ -155,13 +138,6 @@ ve.init.mw.MobileArticleTarget.prototype.attachToolbarSaveButton = function () {
this.toolbar.$actions.append( this.actionsToolbar.$element, this.toolbarSaveButton.$element ); this.toolbar.$actions.append( this.actionsToolbar.$element, this.toolbarSaveButton.$element );
}; };
/**
* @inheritdoc
*/
ve.init.mw.MobileArticleTarget.prototype.editSource = function () {
this.emit( 'editSource' );
};
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -186,6 +162,12 @@ ve.init.mw.MobileArticleTarget.prototype.scrollToHeading = function ( headingNod
} ); } );
}; };
/**
* Close the mobile editor
*/
ve.init.mw.MobileArticleTarget.prototype.close = function () {
};
/** /**
* Back tool * Back tool
*/ */
@ -195,7 +177,7 @@ ve.ui.MWBackTool = function VeUiMwBackTool() {
}; };
OO.inheritClass( ve.ui.MWBackTool, ve.ui.Tool ); OO.inheritClass( ve.ui.MWBackTool, ve.ui.Tool );
ve.ui.MWBackTool.static.name = 'back'; ve.ui.MWBackTool.static.name = 'back';
ve.ui.MWBackTool.static.group = 'history'; ve.ui.MWBackTool.static.group = 'navigation';
ve.ui.MWBackTool.static.icon = 'previous'; ve.ui.MWBackTool.static.icon = 'previous';
ve.ui.MWBackTool.static.title = ve.ui.MWBackTool.static.title =
OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' ); OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' );
@ -211,6 +193,6 @@ ve.ui.MWBackCommand = function VeUiMwBackCommmand() {
}; };
OO.inheritClass( ve.ui.MWBackCommand, ve.ui.Command ); OO.inheritClass( ve.ui.MWBackCommand, ve.ui.Command );
ve.ui.MWBackCommand.prototype.execute = function () { ve.ui.MWBackCommand.prototype.execute = function () {
ve.init.target.emit( 'back' ); ve.init.target.close();
}; };
ve.ui.commandRegistry.register( new ve.ui.MWBackCommand() ); ve.ui.commandRegistry.register( new ve.ui.MWBackCommand() );

View file

@ -42,6 +42,7 @@ ve.init.mw.Target = function VeInitMwTarget( pageName, revisionId, config ) {
this.revid = revisionId || mw.config.get( 'wgCurRevisionId' ); this.revid = revisionId || mw.config.get( 'wgCurRevisionId' );
this.restoring = !!revisionId; this.restoring = !!revisionId;
this.pageDeletedWarning = false;
this.editToken = mw.user.tokens.get( 'editToken' ); this.editToken = mw.user.tokens.get( 'editToken' );
this.submitUrl = ( new mw.Uri( mw.util.getUrl( this.pageName ) ) ) this.submitUrl = ( new mw.Uri( mw.util.getUrl( this.pageName ) ) )
.extend( { action: 'submit' } ); .extend( { action: 'submit' } );
@ -72,31 +73,16 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target );
/** /**
* @event save * @event save
* @param {string} html Rendered page HTML from server
* @param {string} categoriesHtml Rendered categories HTML from server
* @param {number} [newid] New revision id, undefined if unchanged
* @param {boolean} isRedirect Whether this page is now a redirect or not.
*/ */
/** /**
* @event showChanges * @event showChanges
* @param {string} diff
*/ */
/** /**
* @event noChanges * @event noChanges
*/ */
/**
* @event saveAsyncBegin
* Fired when we're waiting for network
*/
/**
* @event saveAsyncComplete
* Fired when we're no longer waiting for network
*/
/** /**
* @event saveErrorEmpty * @event saveErrorEmpty
* Fired when save API returns no data object * Fired when save API returns no data object
@ -105,13 +91,11 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target );
/** /**
* @event saveErrorSpamBlacklist * @event saveErrorSpamBlacklist
* Fired when save is considered spam or blacklisted * Fired when save is considered spam or blacklisted
* @param {Object} editApi
*/ */
/** /**
* @event saveErrorAbuseFilter * @event saveErrorAbuseFilter
* Fired when AbuseFilter throws warnings * Fired when AbuseFilter throws warnings
* @param {Object} editApi
*/ */
/** /**
@ -123,20 +107,16 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target );
/** /**
* @event saveErrorNewUser * @event saveErrorNewUser
* Fired when user is logged in as a new user * Fired when user is logged in as a new user
* @param {string|null} username Name of newly logged-in user, or null if anonymous
*/ */
/** /**
* @event saveErrorCaptcha * @event saveErrorCaptcha
* Fired when saveError indicates captcha field is required * Fired when saveError indicates captcha field is required
* @param {Object} editApi
*/ */
/** /**
* @event saveErrorUnknown * @event saveErrorUnknown
* Fired for any other type of save error * Fired for any other type of save error
* @param {Object} editApi
* @param {Object|null} data API response data
*/ */
/** /**
@ -151,29 +131,14 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target );
/** /**
* @event loadError * @event loadError
* @param {string} errorTypeText
* @param {Object|string} error
*/
/**
* @event saveError
* @param {jqXHR|null} jqXHR
* @param {string} status Text status message
* @param {Object|null} data API response data
*/ */
/** /**
* @event showChangesError * @event showChangesError
* @param {jqXHR|null} jqXHR
* @param {string} status Text status message
* @param {Mixed|null} error HTTP status text
*/ */
/** /**
* @event serializeError * @event serializeError
* @param {jqXHR|null} jqXHR
* @param {string} status Text status message
* @param {Mixed|null} error HTTP status text
*/ */
/** /**
@ -312,7 +277,6 @@ ve.init.mw.Target.static.fixBase = function ( doc ) {
* @method * @method
* @param {Object} response API response data * @param {Object} response API response data
* @param {string} status Text status message * @param {string} status Text status message
* @fires loadError
*/ */
ve.init.mw.Target.prototype.loadSuccess = function ( response ) { ve.init.mw.Target.prototype.loadSuccess = function ( response ) {
var i, len, linkData, aboutDoc, docRevIdMatches, var i, len, linkData, aboutDoc, docRevIdMatches,
@ -454,9 +418,9 @@ ve.init.mw.Target.prototype.onSurfaceReady = function () {
* @param {Object} error Object containing xhr, textStatus and exception keys * @param {Object} error Object containing xhr, textStatus and exception keys
* @fires loadError * @fires loadError
*/ */
ve.init.mw.Target.prototype.loadFail = function ( errorText, error ) { ve.init.mw.Target.prototype.loadFail = function () {
this.loading = false; this.loading = false;
this.emit( 'loadError', errorText, error ); this.emit( 'loadError' );
}; };
/** /**
@ -469,8 +433,6 @@ ve.init.mw.Target.prototype.loadFail = function ( errorText, error ) {
* @param {Object} saveData Options that were used * @param {Object} saveData Options that were used
* @param {Object} response Response data * @param {Object} response Response data
* @param {string} status Text status message * @param {string} status Text status message
* @fires editConflict
* @fires save
*/ */
ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) { ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) {
this.saving = false; this.saving = false;
@ -483,8 +445,7 @@ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) {
} else if ( typeof data.content !== 'string' ) { } else if ( typeof data.content !== 'string' ) {
this.saveFail( doc, saveData, null, 'Invalid HTML content in response from server', response ); this.saveFail( doc, saveData, null, 'Invalid HTML content in response from server', response );
} else { } else {
this.emit( this.saveComplete(
'save',
data.content, data.content,
data.categorieshtml, data.categorieshtml,
data.newrevid, data.newrevid,
@ -496,6 +457,24 @@ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) {
} }
}; };
/**
* Handle successful DOM save event.
*
* @method
* @param {string} html Rendered page HTML from server
* @param {string} categoriesHtml Rendered categories HTML from server
* @param {number} newid New revision id, undefined if unchanged
* @param {boolean} isRedirect Whether this page is a redirect or not
* @param {string} displayTitle What HTML to show as the page title
* @param {Object} lastModified Object containing user-formatted date
* and time strings, or undefined if we made no change.
* @param {string} contentSub HTML to show as the content subtitle
* @fires save
*/
ve.init.mw.Target.prototype.saveComplete = function () {
this.emit( 'save' );
};
/** /**
* Handle an unsuccessful save request. * Handle an unsuccessful save request.
* *
@ -505,22 +484,17 @@ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) {
* @param {Object} jqXHR * @param {Object} jqXHR
* @param {string} status Text status message * @param {string} status Text status message
* @param {Object|null} data API response data * @param {Object|null} data API response data
* @fires saveErrorEmpty
* @fires saveErrorSpamBlacklist
* @fires saveErrorAbuseFilter
* @fires saveErrorBadToken
* @fires saveErrorNewUser
* @fires saveErrorCaptcha
* @fires saveErrorUnknown
*/ */
ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status, data ) { ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status, data ) {
var api, editApi, var api, editApi,
target = this; target = this;
this.saving = false; this.saving = false;
this.pageDeletedWarning = false;
// Handle empty response // Handle empty response
if ( !data ) { if ( !data ) {
this.emit( 'saveErrorEmpty' ); this.saveErrorEmpty();
return; return;
} }
@ -528,14 +502,14 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
// Handle spam blacklist error (either from core or from Extension:SpamBlacklist) // Handle spam blacklist error (either from core or from Extension:SpamBlacklist)
if ( editApi && editApi.spamblacklist ) { if ( editApi && editApi.spamblacklist ) {
this.emit( 'saveErrorSpamBlacklist', editApi ); this.saveErrorSpamBlacklist( editApi );
return; return;
} }
// Handle warnings/errors from Extension:AbuseFilter // Handle warnings/errors from Extension:AbuseFilter
// TODO: Move this to a plugin // TODO: Move this to a plugin
if ( editApi && editApi.info && editApi.info.indexOf( 'Hit AbuseFilter:' ) === 0 && editApi.warning ) { if ( editApi && editApi.info && editApi.info.indexOf( 'Hit AbuseFilter:' ) === 0 && editApi.warning ) {
this.emit( 'saveErrorAbuseFilter', editApi ); this.saveErrorAbuseFilter( editApi );
return; return;
} }
@ -556,7 +530,7 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
intoken: 'edit' intoken: 'edit'
} ) } )
.always( function () { .always( function () {
target.emit( 'saveErrorBadToken' ); target.saveErrorBadToken();
} ) } )
.done( function ( data ) { .done( function ( data ) {
var userMsg, var userMsg,
@ -588,7 +562,7 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
// functions like mw.user.isAnon rely on this. // functions like mw.user.isAnon rely on this.
wgUserName: null wgUserName: null
} ); } );
target.emit( 'saveErrorNewUser', null ); target.saveErrorNewUser( null );
} else { } else {
// New session is a different user // New session is a different user
mw.config.set( { wgUserId: userInfo.id, wgUserName: userInfo.name } ); mw.config.set( { wgUserId: userInfo.id, wgUserName: userInfo.name } );
@ -598,20 +572,20 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
mw.messages.get( 'visualeditor-savedialog-identify-user' ) mw.messages.get( 'visualeditor-savedialog-identify-user' )
.replace( /\$1/g, userInfo.name ) .replace( /\$1/g, userInfo.name )
); );
target.emit( 'saveErrorNewUser', userInfo.name ); target.saveErrorNewUser( userInfo.name );
} }
} }
} }
} ); } );
return; return;
} else if ( data.error && data.error.code === 'editconflict' ) { } else if ( data.error && data.error.code === 'editconflict' ) {
this.emit( 'editConflict' ); this.editConflict();
return; return;
} else if ( data.error && data.error.code === 'pagedeleted' ) { } else if ( data.error && data.error.code === 'pagedeleted' ) {
this.emit( 'saveErrorPageDeleted' ); this.saveErrorPageDeleted();
return; return;
} else if ( data.error && data.error.code === 'titleblacklist-forbidden-edit' ) { } else if ( data.error && data.error.code === 'titleblacklist-forbidden-edit' ) {
this.emit( 'saveErrorTitleBlacklist' ); this.saveErrorTitleBlacklist();
return; return;
} }
@ -629,12 +603,12 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
editApi.captcha.type === 'math' || editApi.captcha.type === 'math' ||
editApi.captcha.type === 'question' editApi.captcha.type === 'question'
) ) { ) ) {
this.emit( 'saveErrorCaptcha', editApi ); this.saveErrorCaptcha( editApi );
return; return;
} }
// Handle (other) unknown and/or unrecoverable errors // Handle (other) unknown and/or unrecoverable errors
this.emit( 'saveErrorUnknown', editApi, data ); this.saveErrorUnknown( editApi, data );
}; };
/** /**
@ -643,8 +617,6 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status,
* @method * @method
* @param {Object} response API response data * @param {Object} response API response data
* @param {string} status Text status message * @param {string} status Text status message
* @fires showChanges
* @fires noChanges
*/ */
ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) { ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) {
var data = response.visualeditor; var data = response.visualeditor;
@ -656,7 +628,7 @@ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) {
null, 'Unsuccessful request: ' + response.error.info, null null, 'Unsuccessful request: ' + response.error.info, null
); );
} else if ( data.result === 'nochanges' ) { } else if ( data.result === 'nochanges' ) {
this.emit( 'noChanges' ); this.noChanges();
} else if ( data.result !== 'success' ) { } else if ( data.result !== 'success' ) {
this.showChangesFail( null, 'Failed request: ' + data.result, null ); this.showChangesFail( null, 'Failed request: ' + data.result, null );
} else if ( typeof data.diff !== 'string' ) { } else if ( typeof data.diff !== 'string' ) {
@ -664,10 +636,20 @@ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) {
null, 'Invalid HTML content in response from server', null null, 'Invalid HTML content in response from server', null
); );
} else { } else {
this.emit( 'showChanges', data.diff ); this.showChangesDiff( data.diff );
} }
}; };
/**
* Show changes diff HTML
*
* @param {string} diffHtml Diff HTML
* @fires showChanges
*/
ve.init.mw.Target.prototype.showChangesDiff = function () {
this.emit( 'showChanges' );
};
/** /**
* Handle errors during showChanges action. * Handle errors during showChanges action.
* *
@ -678,9 +660,171 @@ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) {
* @param {Mixed} error HTTP status text * @param {Mixed} error HTTP status text
* @fires showChangesError * @fires showChangesError
*/ */
ve.init.mw.Target.prototype.showChangesFail = function ( jqXHR, status, error ) { ve.init.mw.Target.prototype.showChangesFail = function () {
this.diffing = false; this.diffing = false;
this.emit( 'showChangesError', jqXHR, status, error ); this.emit( 'showChangesError' );
};
/**
* Show an save process error message
*
* @method
* @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of
* Node objects)
* @param {boolean} [allowReapply=true] Whether or not to allow the user to reapply.
* Reset when swapping panels. Assumed to be true unless explicitly set to false.
* @param {boolean} [warning=false] Whether or not this is a warning.
*/
ve.init.mw.Target.prototype.showSaveError = function () {
};
/**
* Handle general save error
*
* @method
* @fires saveErrorEmpty
*/
ve.init.mw.Target.prototype.saveErrorEmpty = function () {
this.showSaveError( ve.msg( 'visualeditor-saveerror', 'Empty server response' ), false /* prevents reapply */ );
this.emit( 'saveErrorEmpty' );
};
/**
* Handle spam blacklist error
*
* @method
* @param {Object} editApi
* @fires saveErrorSpamBlacklist
*/
ve.init.mw.Target.prototype.saveErrorSpamBlacklist = function ( editApi ) {
this.showSaveError(
$( $.parseHTML( editApi.sberrorparsed ) ),
false // prevents reapply
);
this.emit( 'saveErrorSpamBlacklist' );
};
/**
* Handel abuse filter error
*
* @method
* @param {Object} editApi
* @fires saveErrorAbuseFilter
*/
ve.init.mw.Target.prototype.saveErrorAbuseFilter = function ( editApi ) {
this.showSaveError( $( $.parseHTML( editApi.warning ) ) );
// Don't disable the save button. If the action is not disallowed the user may save the
// edit by pressing Save again. The AbuseFilter API currently has no way to distinguish
// between filter triggers that are and aren't disallowing the action.
this.emit( 'saveErrorAbuseFilter' );
};
/**
* Handle title blacklist save error
*
* @method
* @fires saveErrorTitleBlacklist
*/
ve.init.mw.Target.prototype.saveErrorTitleBlacklist = function () {
this.showSaveError( mw.msg( 'visualeditor-saveerror-titleblacklist' ) );
this.emit( 'saveErrorTitleBlacklist' );
};
/**
* Handle token fetch indicating another user is logged in
*
* @method
* @param {string|null} username Name of newly logged-in user, or null if anonymous
* @fires saveErrorNewUser
*/
ve.init.mw.Target.prototype.saveErrorNewUser = function ( username ) {
var badToken, userMsg;
badToken = document.createTextNode( mw.msg( 'visualeditor-savedialog-error-badtoken' ) + ' ' );
// mediawiki.jqueryMsg has a bug with [[User:$1|$1]] (bug 51388)
if ( username === null ) {
userMsg = 'visualeditor-savedialog-identify-anon';
} else {
userMsg = 'visualeditor-savedialog-identify-user---' + username;
}
this.showSaveError(
$( badToken ).add( $.parseHTML( mw.message( userMsg ).parse() ) )
);
this.emit( 'saveErrorNewUser' );
};
/**
* Handle unknown save error
*
* @method
* @param {Object} editApi
* @param {Object|null} data API response data
* @fires onSaveErrorUnknown
*/
ve.init.mw.Target.prototype.saveErrorUnknown = function ( editApi, data ) {
this.showSaveError(
$( document.createTextNode(
( editApi && editApi.info ) ||
( data.error && data.error.info ) ||
( editApi && editApi.code ) ||
( data.error && data.error.code ) ||
'Unknown error'
) ),
false // prevents reapply
);
this.emit( 'onSaveErrorUnknown' );
};
/**
* Handle a bad token
*
* @method
* @fires saveErrorBadToken
*/
ve.init.mw.Target.prototype.saveErrorBadToken = function () {
this.emit( 'saveErrorBadToken' );
};
/**
* Handle captcha error
*
* @method
* @param {Object} editApi
* @fires saveErrorCaptcha
*/
ve.init.mw.Target.prototype.saveErrorCaptcha = function () {
this.emit( 'saveErrorCaptcha' );
};
/**
* Handle page deleted error
*
* @method
* @fires saveErrorPageDeleted
*/
ve.init.mw.Target.prototype.saveErrorPageDeleted = function () {
this.pageDeletedWarning = true;
this.showSaveError( mw.msg( 'visualeditor-recreate', mw.msg( 'ooui-dialog-process-continue' ) ), true, true );
this.emit( 'saveErrorPageDeleted' );
};
/**
* Handle an edit conflict
*
* @method
* @fires editConflict
*/
ve.init.mw.Target.prototype.editConflict = function () {
this.emit( 'editConflict' );
};
/**
* Handle no changes in diff
*
* @method
* @fires noChanges
*/
ve.init.mw.Target.prototype.noChanges = function () {
this.emit( 'noChanges' );
}; };
/** /**
@ -694,19 +838,19 @@ ve.init.mw.Target.prototype.showChangesFail = function ( jqXHR, status, error )
* @param {string} status Text status message * @param {string} status Text status message
* @fires serializeComplete * @fires serializeComplete
*/ */
ve.init.mw.Target.prototype.onSerialize = function ( response ) { ve.init.mw.Target.prototype.serializeSuccess = function ( response ) {
this.serializing = false; this.serializing = false;
var data = response.visualeditor; var data = response.visualeditor;
if ( !data && !response.error ) { if ( !data && !response.error ) {
this.onSerializeError( null, 'Invalid response from server', null ); this.serializeFail( null, 'Invalid response from server', null );
} else if ( response.error ) { } else if ( response.error ) {
this.onSerializeError( this.serializeFail(
null, 'Unsuccessful request: ' + response.error.info, null null, 'Unsuccessful request: ' + response.error.info, null
); );
} else if ( data.result === 'error' ) { } else if ( data.result === 'error' ) {
this.onSerializeError( null, 'Server error', null ); this.serializeFail( null, 'Server error', null );
} else if ( typeof data.content !== 'string' ) { } else if ( typeof data.content !== 'string' ) {
this.onSerializeError( this.serializeFail(
null, 'No Wikitext content in response from server', null null, 'No Wikitext content in response from server', null
); );
} else { } else {
@ -729,9 +873,9 @@ ve.init.mw.Target.prototype.onSerialize = function ( response ) {
* @param {Mixed|null} error HTTP status text * @param {Mixed|null} error HTTP status text
* @fires serializeError * @fires serializeError
*/ */
ve.init.mw.Target.prototype.onSerializeError = function ( jqXHR, status, error ) { ve.init.mw.Target.prototype.serializeFail = function () {
this.serializing = false; this.serializing = false;
this.emit( 'serializeError', jqXHR, status, error ); this.emit( 'serializeError' );
}; };
/** /**
@ -1129,6 +1273,28 @@ ve.init.mw.Target.prototype.tryWithPreparedCacheKey = function ( doc, options, e
return preparedCacheKey.then( ajaxRequest, ajaxRequest ); return preparedCacheKey.then( ajaxRequest, ajaxRequest );
}; };
/**
* Prepare to save the article
*
* @fires saveInitiated
*/
ve.init.mw.Target.prototype.startSave = function () {
this.emit( 'saveInitiated' );
if ( !this.docToSave ) {
this.docToSave = this.getSurface().getDom();
}
this.save( this.docToSave, this.getSaveOptions() );
};
/**
* Get edit API options from the save dialog form.
*
* @returns {Object} Save options for submission to the MediaWiki API
*/
ve.init.mw.Target.prototype.getSaveOptions = function () {
return {};
};
/** /**
* Post DOM data to the Parsoid API. * Post DOM data to the Parsoid API.
* *
@ -1260,8 +1426,8 @@ ve.init.mw.Target.prototype.serialize = function ( doc, callback ) {
page: this.pageName, page: this.pageName,
oldid: this.revid oldid: this.revid
}, 'serialize' ) }, 'serialize' )
.done( ve.init.mw.Target.prototype.onSerialize.bind( this ) ) .done( ve.init.mw.Target.prototype.serializeSuccess.bind( this ) )
.fail( ve.init.mw.Target.prototype.onSerializeError.bind( this ) ); .fail( ve.init.mw.Target.prototype.serializeFail.bind( this ) );
return true; return true;
}; };
@ -1391,20 +1557,26 @@ ve.init.mw.Target.prototype.updateToolbarSaveButtonState = function () {
/** /**
* Handle clicks on the save button in the toolbar. * Handle clicks on the save button in the toolbar.
*
* @fires saveBegin
*/ */
ve.init.mw.Target.prototype.onToolbarSaveButtonClick = function () { ve.init.mw.Target.prototype.onToolbarSaveButtonClick = function () {
if ( this.edited || this.restoring ) { if ( this.edited || this.restoring ) {
this.showSaveDialog(); this.showSaveDialog();
this.emit( 'saveBegin' );
} }
}; };
/** /**
* Show a save dialog * Show a save dialog
*
* @fires saveWorkflowBegin
*/ */
ve.init.mw.Target.prototype.showSaveDialog = function () { ve.init.mw.Target.prototype.showSaveDialog = function () {
this.emit( 'saveWorkflowBegin' );
// Preload the serialization
if ( !this.docToSave ) {
this.docToSave = this.getSurface().getDom();
}
this.prepareCacheKey( this.docToSave );
}; };
/** /**