diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js index 602a6923e6..5cd3feba0c 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js @@ -8,7 +8,7 @@ /*global confirm, alert */ /** - * Initialization MediaWiki view page target. + * MediaWiki desktop article target. * * @class * @extends ve.init.mw.Target @@ -44,6 +44,8 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config this.toolbarSetupDeferred = null; this.welcomeDialog = null; this.welcomeDialogPromise = null; + this.captcha = null; + this.docToSave = null; // 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 @@ -66,24 +68,6 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config this.originalDocumentTitle = document.title; 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 this.$element.addClass( 've-init-mw-desktopArticleTarget' ); @@ -457,15 +441,11 @@ ve.init.mw.DesktopArticleTarget.prototype.cancel = function ( trackMechanism ) { }; /** - * Handle failed DOM load event. - * - * @method - * @param {string} errorTypeText - * @param {string} error + * @inheritdoc */ ve.init.mw.DesktopArticleTarget.prototype.loadFail = function ( errorText, error ) { // 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 // 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 - * - * @method + * @inheritdoc */ ve.init.mw.DesktopArticleTarget.prototype.onSurfaceReady = function () { var surfaceReadyTime = ve.now(), @@ -590,20 +568,14 @@ ve.init.mw.DesktopArticleTarget.prototype.onViewTabClick = function ( e ) { }; /** - * 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. + * @inheritdoc */ -ve.init.mw.DesktopArticleTarget.prototype.onSave = function ( +ve.init.mw.DesktopArticleTarget.prototype.saveComplete = function ( html, categoriesHtml, newid, isRedirect, displayTitle, lastModified, contentSub ) { + // Parent method + ve.init.mw.DesktopArticleTarget.super.prototype.saveComplete.apply( this, arguments ); + var newUrlParams, watchChecked; this.saveDeferred.resolve(); if ( !this.pageExists || this.restoring ) { @@ -676,82 +648,7 @@ ve.init.mw.DesktopArticleTarget.prototype.onSave = function ( /** * @inheritdoc */ -ve.init.mw.DesktopArticleTarget.prototype.saveFail = function () { - 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 ) { +ve.init.mw.DesktopArticleTarget.prototype.saveErrorCaptcha = function ( editApi ) { var $captchaDiv = $( '
' ); @@ -799,38 +696,6 @@ ve.init.mw.DesktopArticleTarget.prototype.onSaveErrorCaptcha = function ( editAp 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 * 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 - * - * @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. + * @inheritdoc */ ve.init.mw.DesktopArticleTarget.prototype.showSaveError = function ( msg, allowReapply, warning ) { this.saveDeferred.reject( [ new OO.ui.Error( msg, { recoverable: allowReapply, warning: warning } ) ] ); }; /** - * Handle Show changes event. - * - * @method - * @param {string} diffHtml + * @inheritdoc */ -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 this.getSurface().getModel().getDocument().once( 'transact', this.saveDialog.clearDiff.bind( this.saveDialog ) @@ -871,25 +729,23 @@ ve.init.mw.DesktopArticleTarget.prototype.onShowChanges = function ( diffHtml ) }; /** - * Handle failed show changes event. - * - * @method - * @param {Object} jqXHR - * @param {string} status Text status message + * @inheritdoc */ -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 ) ); this.saveDialog.popPending(); }; /** - * Called if a call to target.serialize() failed. - * - * @method - * @param {jqXHR|null} jqXHR - * @param {string} status Text status message + * @inheritdoc */ -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 ) ); this.getSurface().getDialogs().closeWindow( 'wikitextswitchconfirm' ); @@ -902,21 +758,17 @@ ve.init.mw.DesktopArticleTarget.prototype.onSerializeError = function ( jqXHR, s }; /** - * Handle edit conflict event. - * - * @method + * @inheritdoc */ -ve.init.mw.DesktopArticleTarget.prototype.onEditConflict = function () { +ve.init.mw.DesktopArticleTarget.prototype.editConflict = function () { this.saveDialog.popPending(); this.saveDialog.swapPanel( 'conflict' ); }; /** - * Handle failed show changes event. - * - * @method + * @inheritdoc */ -ve.init.mw.DesktopArticleTarget.prototype.onNoChanges = function () { +ve.init.mw.DesktopArticleTarget.prototype.noChanges = function () { this.saveDialog.popPending(); this.saveDialog.swapPanel( 'nochanges' ); 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.pushPending(); if ( this.pageExists ) { - // Has no callback, handled via target.onShowChanges + // Has no callback, handled via target.showChangesDiff this.showChanges( this.docToSave ); } else { 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. - * @fires saveInitiated + * @inheritdoc * @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save * succeeds/fails. */ -ve.init.mw.DesktopArticleTarget.prototype.saveDocument = function ( saveDeferred ) { +ve.init.mw.DesktopArticleTarget.prototype.startSave = function ( saveDeferred ) { if ( this.deactivating ) { return false; } - var saveOptions = this.getSaveOptions(); - this.emit( 'saveInitiated' ); - // Reset any old captcha data if ( this.captcha ) { this.saveDialog.clearMessage( 'captcha' ); delete this.captcha; } + var saveOptions = this.getSaveOptions(); + if ( +mw.user.options.get( 'forceeditsummary' ) && saveOptions.summary === '' && @@ -1001,7 +851,8 @@ ve.init.mw.DesktopArticleTarget.prototype.saveDocument = function ( saveDeferred ); this.saveDialog.popPending(); } else { - this.save( this.docToSave, saveOptions ); + // Parent method + ve.init.mw.DesktopArticleTarget.super.prototype.startSave.call( this ); this.saveDeferred = saveDeferred; } }; @@ -1076,9 +927,7 @@ ve.init.mw.DesktopArticleTarget.prototype.submitWithSaveFields = function ( fiel }; /** - * Get edit API options from the save dialog form. - * - * @returns {Object} Save options for submission to the MediaWiki API + * @inheritdoc */ ve.init.mw.DesktopArticleTarget.prototype.getSaveOptions = function () { var key, @@ -1197,20 +1046,14 @@ ve.init.mw.DesktopArticleTarget.prototype.attachToolbarSaveButton = function () /** * @inheritdoc - * @fires saveWorkflowBegin */ ve.init.mw.DesktopArticleTarget.prototype.showSaveDialog = function () { + // Parent method + ve.init.mw.DesktopArticleTarget.super.prototype.showSaveDialog.call( this ); + var target = this, 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 this.getSurface().getDialogs().getWindow( 'mwSave' ).done( function ( win ) { if ( !target.saveDialog ) { @@ -1218,7 +1061,7 @@ ve.init.mw.DesktopArticleTarget.prototype.showSaveDialog = function () { // Connect to save dialog target.saveDialog.connect( target, { - save: 'saveDocument', + save: 'startSave', review: 'onSaveDialogReview', resolve: 'onSaveDialogResolveConflict', retry: 'onSaveRetry', @@ -1430,10 +1273,10 @@ ve.init.mw.DesktopArticleTarget.prototype.onWindowPopState = function ( e ) { * @method * @param {string} html Rendered 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 - and time strings, or undefined if we made no change. - * @param {string} contentSub What HTML to show as the content subtitle + * and time strings, or undefined if we made no change. + * @param {string} contentSub HTML to show as the content subtitle */ ve.init.mw.DesktopArticleTarget.prototype.replacePageContent = function ( html, categoriesHtml, displayTitle, lastModified, contentSub diff --git a/modules/ve-mw/init/targets/ve.init.mw.MobileArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.MobileArticleTarget.js index d6f0255df1..c8410d51ed 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.MobileArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.MobileArticleTarget.js @@ -6,12 +6,12 @@ */ /** + * MediaWiki mobile article target. * * @class * @extends ve.init.mw.Target * * @constructor - * @param {jQuery} $container Container to render target into * @param {Object} [config] Configuration options * @cfg {number} [section] Number of the section target should scroll to * @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 ); -/* Events */ - -/** - * @event back - * Leave the editor - */ - -/** - * @event editSource - * Switch to edit source mode - */ - /* Static Properties */ ve.init.mw.MobileArticleTarget.static.toolbarGroups = [ @@ -84,7 +72,7 @@ ve.init.mw.MobileArticleTarget.static.name = 'mobile'; /* Methods */ /** - * Once surface is ready ready, init UI. + * @inheritdoc */ ve.init.mw.MobileArticleTarget.prototype.onSurfaceReady = function () { // Parent method @@ -94,12 +82,7 @@ ve.init.mw.MobileArticleTarget.prototype.onSurfaceReady = function () { }; /** - * Create a surface. - * - * @method - * @param {ve.dm.Document} dmDoc Document model - * @param {Object} [config] Configuration options - * @returns {ve.ui.MobileSurface} + * @inheritdoc */ ve.init.mw.MobileArticleTarget.prototype.createSurface = function ( 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 ); }; -/** - * @inheritdoc - */ -ve.init.mw.MobileArticleTarget.prototype.editSource = function () { - this.emit( 'editSource' ); -}; - /** * @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 */ @@ -195,7 +177,7 @@ ve.ui.MWBackTool = function VeUiMwBackTool() { }; OO.inheritClass( ve.ui.MWBackTool, ve.ui.Tool ); 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.title = OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' ); @@ -211,6 +193,6 @@ ve.ui.MWBackCommand = function VeUiMwBackCommmand() { }; OO.inheritClass( ve.ui.MWBackCommand, ve.ui.Command ); ve.ui.MWBackCommand.prototype.execute = function () { - ve.init.target.emit( 'back' ); + ve.init.target.close(); }; ve.ui.commandRegistry.register( new ve.ui.MWBackCommand() ); diff --git a/modules/ve-mw/init/ve.init.mw.Target.js b/modules/ve-mw/init/ve.init.mw.Target.js index d842d58d3a..6f0853745c 100644 --- a/modules/ve-mw/init/ve.init.mw.Target.js +++ b/modules/ve-mw/init/ve.init.mw.Target.js @@ -42,6 +42,7 @@ ve.init.mw.Target = function VeInitMwTarget( pageName, revisionId, config ) { this.revid = revisionId || mw.config.get( 'wgCurRevisionId' ); this.restoring = !!revisionId; + this.pageDeletedWarning = false; this.editToken = mw.user.tokens.get( 'editToken' ); this.submitUrl = ( new mw.Uri( mw.util.getUrl( this.pageName ) ) ) .extend( { action: 'submit' } ); @@ -72,31 +73,16 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target ); /** * @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 - * @param {string} diff */ /** * @event noChanges */ -/** - * @event saveAsyncBegin - * Fired when we're waiting for network - */ - -/** - * @event saveAsyncComplete - * Fired when we're no longer waiting for network - */ - /** * @event saveErrorEmpty * Fired when save API returns no data object @@ -105,13 +91,11 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target ); /** * @event saveErrorSpamBlacklist * Fired when save is considered spam or blacklisted - * @param {Object} editApi */ /** * @event saveErrorAbuseFilter * Fired when AbuseFilter throws warnings - * @param {Object} editApi */ /** @@ -123,20 +107,16 @@ OO.inheritClass( ve.init.mw.Target, ve.init.Target ); /** * @event saveErrorNewUser * 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 * Fired when saveError indicates captcha field is required - * @param {Object} editApi */ /** * @event saveErrorUnknown * 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 - * @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 - * @param {jqXHR|null} jqXHR - * @param {string} status Text status message - * @param {Mixed|null} error HTTP status text */ /** * @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 * @param {Object} response API response data * @param {string} status Text status message - * @fires loadError */ ve.init.mw.Target.prototype.loadSuccess = function ( response ) { 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 * @fires loadError */ -ve.init.mw.Target.prototype.loadFail = function ( errorText, error ) { +ve.init.mw.Target.prototype.loadFail = function () { 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} response Response data * @param {string} status Text status message - * @fires editConflict - * @fires save */ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) { this.saving = false; @@ -483,8 +445,7 @@ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) { } else if ( typeof data.content !== 'string' ) { this.saveFail( doc, saveData, null, 'Invalid HTML content in response from server', response ); } else { - this.emit( - 'save', + this.saveComplete( data.content, data.categorieshtml, 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. * @@ -505,22 +484,17 @@ ve.init.mw.Target.prototype.saveSuccess = function ( doc, saveData, response ) { * @param {Object} jqXHR * @param {string} status Text status message * @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 ) { var api, editApi, target = this; + this.saving = false; + this.pageDeletedWarning = false; // Handle empty response if ( !data ) { - this.emit( 'saveErrorEmpty' ); + this.saveErrorEmpty(); 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) if ( editApi && editApi.spamblacklist ) { - this.emit( 'saveErrorSpamBlacklist', editApi ); + this.saveErrorSpamBlacklist( editApi ); return; } // Handle warnings/errors from Extension:AbuseFilter // TODO: Move this to a plugin if ( editApi && editApi.info && editApi.info.indexOf( 'Hit AbuseFilter:' ) === 0 && editApi.warning ) { - this.emit( 'saveErrorAbuseFilter', editApi ); + this.saveErrorAbuseFilter( editApi ); return; } @@ -556,7 +530,7 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status, intoken: 'edit' } ) .always( function () { - target.emit( 'saveErrorBadToken' ); + target.saveErrorBadToken(); } ) .done( function ( data ) { 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. wgUserName: null } ); - target.emit( 'saveErrorNewUser', null ); + target.saveErrorNewUser( null ); } else { // New session is a different user 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' ) .replace( /\$1/g, userInfo.name ) ); - target.emit( 'saveErrorNewUser', userInfo.name ); + target.saveErrorNewUser( userInfo.name ); } } } } ); return; } else if ( data.error && data.error.code === 'editconflict' ) { - this.emit( 'editConflict' ); + this.editConflict(); return; } else if ( data.error && data.error.code === 'pagedeleted' ) { - this.emit( 'saveErrorPageDeleted' ); + this.saveErrorPageDeleted(); return; } else if ( data.error && data.error.code === 'titleblacklist-forbidden-edit' ) { - this.emit( 'saveErrorTitleBlacklist' ); + this.saveErrorTitleBlacklist(); return; } @@ -629,12 +603,12 @@ ve.init.mw.Target.prototype.saveFail = function ( doc, saveData, jqXHR, status, editApi.captcha.type === 'math' || editApi.captcha.type === 'question' ) ) { - this.emit( 'saveErrorCaptcha', editApi ); + this.saveErrorCaptcha( editApi ); return; } // 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 * @param {Object} response API response data * @param {string} status Text status message - * @fires showChanges - * @fires noChanges */ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) { var data = response.visualeditor; @@ -656,7 +628,7 @@ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) { null, 'Unsuccessful request: ' + response.error.info, null ); } else if ( data.result === 'nochanges' ) { - this.emit( 'noChanges' ); + this.noChanges(); } else if ( data.result !== 'success' ) { this.showChangesFail( null, 'Failed request: ' + data.result, null ); } 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 ); } 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. * @@ -678,9 +660,171 @@ ve.init.mw.Target.prototype.showChangesSuccess = function ( response ) { * @param {Mixed} error HTTP status text * @fires showChangesError */ -ve.init.mw.Target.prototype.showChangesFail = function ( jqXHR, status, error ) { +ve.init.mw.Target.prototype.showChangesFail = function () { 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 * @fires serializeComplete */ -ve.init.mw.Target.prototype.onSerialize = function ( response ) { +ve.init.mw.Target.prototype.serializeSuccess = function ( response ) { this.serializing = false; var data = response.visualeditor; if ( !data && !response.error ) { - this.onSerializeError( null, 'Invalid response from server', null ); + this.serializeFail( null, 'Invalid response from server', null ); } else if ( response.error ) { - this.onSerializeError( + this.serializeFail( null, 'Unsuccessful request: ' + response.error.info, null ); } else if ( data.result === 'error' ) { - this.onSerializeError( null, 'Server error', null ); + this.serializeFail( null, 'Server error', null ); } else if ( typeof data.content !== 'string' ) { - this.onSerializeError( + this.serializeFail( null, 'No Wikitext content in response from server', null ); } else { @@ -729,9 +873,9 @@ ve.init.mw.Target.prototype.onSerialize = function ( response ) { * @param {Mixed|null} error HTTP status text * @fires serializeError */ -ve.init.mw.Target.prototype.onSerializeError = function ( jqXHR, status, error ) { +ve.init.mw.Target.prototype.serializeFail = function () { 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 ); }; +/** + * 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. * @@ -1260,8 +1426,8 @@ ve.init.mw.Target.prototype.serialize = function ( doc, callback ) { page: this.pageName, oldid: this.revid }, 'serialize' ) - .done( ve.init.mw.Target.prototype.onSerialize.bind( this ) ) - .fail( ve.init.mw.Target.prototype.onSerializeError.bind( this ) ); + .done( ve.init.mw.Target.prototype.serializeSuccess.bind( this ) ) + .fail( ve.init.mw.Target.prototype.serializeFail.bind( this ) ); return true; }; @@ -1391,20 +1557,26 @@ ve.init.mw.Target.prototype.updateToolbarSaveButtonState = function () { /** * Handle clicks on the save button in the toolbar. - * - * @fires saveBegin */ ve.init.mw.Target.prototype.onToolbarSaveButtonClick = function () { if ( this.edited || this.restoring ) { this.showSaveDialog(); - this.emit( 'saveBegin' ); } }; /** * Show a save dialog + * + * @fires saveWorkflowBegin */ 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 ); }; /**