| Current File : /home/jvzmxxx/wiki1/extensions/MobileFrontend/resources/mobile.editor.overlay/EditorOverlay.js |
( function ( M, $ ) {
var EditorOverlayBase = M.require( 'mobile.editor.common/EditorOverlayBase' ),
Section = M.require( 'mobile.startup/Section' ),
EditorGateway = M.require( 'mobile.editor.api/EditorGateway' ),
AbuseFilterPanel = M.require( 'mobile.abusefilter/AbuseFilterPanel' ),
settings = M.require( 'mobile.startup/settings' ),
Button = M.require( 'mobile.startup/Button' ),
toast = M.require( 'mobile.startup/toast' ),
MessageBox = M.require( 'mobile.messageBox/MessageBox' );
/**
* Overlay that shows an editor
* @class EditorOverlay
* @uses Section
* @uses AbuseFilterPanel
* @uses EditorGateway
* @uses VisualEditorOverlay
* @extends EditorOverlayBase
*
* @constructor
* @param {Object} options Configuration options
*/
function EditorOverlay( options ) {
this.gateway = new EditorGateway( {
api: options.api,
title: options.title,
sectionId: options.sectionId,
oldId: options.oldId,
isNewPage: options.isNewPage
} );
this.readOnly = !!options.oldId; // If old revision, readOnly mode
if ( this.isVisualEditorEnabled() ) {
options.editSwitcher = true;
}
if ( this.readOnly ) {
options.readOnly = true;
options.editingMsg = mw.msg( 'mobile-frontend-editor-viewing-source-page', options.title );
} else {
options.editingMsg = mw.msg( 'mobile-frontend-editor-editing-page', options.title );
}
if ( options.isAnon ) {
// add required data for anonymous editing warning
options = this._prepareAnonWarning( options );
}
// be explicit here. This may have been initialized from VE.
options.isVisualEditor = false;
options.previewingMsg = mw.msg( 'mobile-frontend-editor-previewing-page', options.title );
EditorOverlayBase.call( this, options );
}
OO.mfExtend( EditorOverlay, EditorOverlayBase, {
/** @inheritdoc **/
isBorderBox: false,
/** @inheritdoc **/
templatePartials: $.extend( {}, EditorOverlayBase.prototype.templatePartials, {
content: mw.template.get( 'mobile.editor.overlay', 'content.hogan' ),
messageBox: MessageBox.prototype.template,
anonWarning: mw.template.get( 'mobile.editor.common', 'EditorOverlayAnonWarning.hogan' )
} ),
/**
* @inheritdoc
* @cfg {Object} defaults Default options hash.
* @cfg {Object} defaults.loginButton options to render an sign in button
* @cfg {Object} defaults.signupButton options to render a sign up button
* @cfg {Object} defaults.anonButton options to render an edit anonymously button
* @cfg {Object} defaults.warningOptions options for a MessageBox to display anonymous message warning
* @cfg {mw.Api} defaults.api an api module to retrieve pages
*/
defaults: $.extend( {}, EditorOverlayBase.prototype.defaults, {
loginButton: new Button( {
block: true,
label: mw.msg( 'mobile-frontend-watchlist-cta-button-login' )
} ).options,
signupButton: new Button( {
block: true,
label: mw.msg( 'mobile-frontend-watchlist-cta-button-signup' )
} ).options,
anonButton: new Button( {
label: mw.msg( 'mobile-frontend-editor-anon' ),
block: true,
additionalClassNames: 'continue anonymous',
progressive: true
} ).options,
warningOptions: {
className: 'warningbox anon-msg',
msg: mw.msg( 'mobile-frontend-editor-anonwarning' )
}
} ),
editor: 'wikitext',
sectionLine: '',
/**
* Check whether VisualEditor is enabled or not.
* @method
* @return {boolean}
*/
isVisualEditorEnabled: function () {
return mw.config.get( 'wgVisualEditorConfig' ) &&
$.inArray( mw.config.get( 'wgNamespaceNumber' ), mw.config.get( 'wgVisualEditorConfig' ).namespaces ) > -1 &&
mw.config.get( 'wgTranslatePageTranslation' ) !== 'translation' &&
mw.config.get( 'wgPageContentModel' ) === 'wikitext';
},
events: $.extend( {}, EditorOverlayBase.prototype.events, {
'input .wikitext-editor': 'onInputWikitextEditor'
} ),
/**
* Wikitext Editor input handler
*/
onInputWikitextEditor: function () {
this.gateway.setContent( this.$( '.wikitext-editor' ).val() );
this.$( '.continue, .submit' ).prop( 'disabled', false );
},
/**
* @inheritdoc
*/
onClickContinue: function ( ev ) {
// handle the click on "Edit without logging in"
if ( this.options.isAnon && $( ev.target ).hasClass( 'anonymous' ) ) {
this._showEditorAfterWarning();
return false;
}
EditorOverlayBase.prototype.onClickContinue.apply( this, arguments );
},
/**
* @inheritdoc
*/
onClickBack: function () {
EditorOverlayBase.prototype.onClickBack.apply( this, arguments );
this._hidePreview();
},
/** @inheritdoc **/
postRender: function () {
var self = this;
if ( this.isVisualEditorEnabled() ) {
mw.loader.using( 'ext.visualEditor.switching' ).then( function () {
var switchToolbar,
toolFactory = new OO.ui.ToolFactory(),
toolGroupFactory = new OO.ui.ToolGroupFactory();
toolFactory.register( mw.libs.ve.MWEditModeVisualTool );
toolFactory.register( mw.libs.ve.MWEditModeSourceTool );
switchToolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory, {
classes: [ 'editor-switcher' ]
} );
switchToolbar.on( 'switchEditor', function ( mode ) {
if ( mode === 'visual' ) {
// If the user tries to switch to the VisualEditor, check if any changes have
// been made, and if so, tell the user they have to save first.
if ( !self.gateway.hasChanged ) {
self._switchToVisualEditor( self.options );
} else {
if ( window.confirm( mw.msg( 'mobile-frontend-editor-switch-confirm' ) ) ) {
self.onStageChanges();
}
}
}
} );
switchToolbar.setup( [
{
type: 'list',
icon: 'edit',
title: mw.msg( 'visualeditor-mweditmode-tooltip' ), // resource-modules-disable-line
include: [ 'editModeVisual', 'editModeSource' ]
}
] );
self.$el.find( '.switcher-container' ).html( switchToolbar.$element );
switchToolbar.emit( 'updateState' );
} );
}
EditorOverlayBase.prototype.postRender.apply( this );
this.$preview = this.$( '.preview' );
this.$content = this.$( '.wikitext-editor' );
if ( self.options.isAnon ) {
this.$anonWarning = this.$( '.anonwarning' );
this.$content.hide();
// the user has to click login, signup or edit without login, disable "Next" button on top right
this.$anonHiddenButtons = this.$( '.overlay-header .continue, .editor-switcher' ).hide();
this.clearSpinner();
}
// make license links open in separate tabs
this.$( '.license a' ).attr( 'target', '_blank' );
this.abuseFilterPanel = new AbuseFilterPanel( {
overlayManager: this.overlayManager
} ).appendTo( this.$( '.panels' ) );
// If in readOnly mode, make textarea readonly
if ( this.readOnly ) {
this.$content.prop( 'readonly', true );
}
this.$content.on( 'input', this._resizeEditor.bind( this ) );
if ( !self.options.isAnon ) {
this._loadContent();
}
},
/**
* Sets additional values used for anonymous editing warning.
* @method
* @private
* @param {Object} options object
* @return {Object} Object with all options
*/
_prepareAnonWarning: function ( options ) {
var params = $.extend( {
// use wgPageName as this includes the namespace if outside Main
returnto: options.returnTo || mw.config.get( 'wgPageName' ),
returntoquery: 'action=edit§ion=' + options.sectionId,
warning: 'mobile-frontend-edit-login-action'
}, options.queryParams ),
signupParams = $.extend( {
type: 'signup',
warning: 'mobile-frontend-edit-signup-action'
}, options.signupQueryParams );
options.loginButton = $.extend( {
href: mw.util.getUrl( 'Special:UserLogin', params )
}, this.defaults.loginButton );
options.signupButton = $.extend( {
href: mw.util.getUrl( 'Special:UserLogin', $.extend( params, signupParams ) )
}, this.defaults.signupButton );
return options;
},
/**
* Handles click on "Edit without login" in anonymous editing warning.
* @method
* @private
*/
_showEditorAfterWarning: function () {
this.showSpinner();
this.$anonWarning.hide();
// reenable "Next" button
this.$anonHiddenButtons.show();
this._loadContent();
},
/**
* Prepares the preview interface and reveals the save screen of the overlay
* @method
* @inheritdoc
*/
onStageChanges: function () {
var self = this,
params = {
text: this.getContent()
};
this.scrollTop = $( 'body' ).scrollTop();
this.$content.hide();
this.showSpinner();
if ( mw.config.get( 'wgIsMainPage' ) ) {
params.mainpage = 1; // Setting it to 0 will have the same effect
}
this.gateway.getPreview( params ).done( function ( parsedText, parsedSectionLine ) {
// On desktop edit summaries strip tags. Mimic this behavior on mobile devices
self.sectionLine = $( '<div/>' ).html( parsedSectionLine ).text();
new Section( {
el: self.$preview,
text: parsedText
} ).$( 'a' ).on( 'click', false );
// Emit event so we can perform enhancements to page
M.emit( 'edit-preview', self );
} ).fail( function () {
self.$preview.addClass( 'error' ).text( mw.msg( 'mobile-frontend-editor-error-preview' ) );
} ).always( function () {
self.clearSpinner();
self.$preview.show();
} );
EditorOverlayBase.prototype.onStageChanges.apply( this, arguments );
},
/**
* Hides the preview and reverts back to initial screen.
* @method
* @private
*/
_hidePreview: function () {
this.gateway.abortPreview();
this.clearSpinner();
this.$preview.removeClass( 'error' ).hide();
this.$content.show();
window.scrollTo( 0, this.scrollTop );
this.showHidden( '.initial-header' );
this.abuseFilterPanel.hide();
},
/**
* Resize the editor textarea, maintaining scroll position in iOS
*/
_resizeEditor: function () {
var scrollTop;
if ( !this.$scrollContainer ) {
this.$scrollContainer = $( OO.ui.Element.static.getClosestScrollableContainer( this.$content[ 0 ] ) );
this.$content.css( 'padding-bottom', this.$scrollContainer.height() * 0.6 );
}
if ( this.$content.prop( 'scrollHeight' ) ) {
scrollTop = this.$scrollContainer.scrollTop();
this.$content
.css( 'height', 'auto' )
// can't reuse prop( 'scrollHeight' ) because we need the current value
.css( 'height', ( this.$content.prop( 'scrollHeight' ) + 2 ) + 'px' );
this.$scrollContainer.scrollTop( scrollTop );
}
},
/**
* Set content to the user input field.
* @param {string} content The content to set.
*/
setContent: function ( content ) {
this.$content
.show()
.val( content );
this._resizeEditor();
},
/**
* Returns the content of the user input field.
* @return {string}
*/
getContent: function () {
return this.$content.val();
},
/**
* Requests content from the API and reveals it in UI.
* @method
* @private
*/
_loadContent: function () {
var self = this;
this.$content.hide();
this.showSpinner();
this.gateway.getContent()
.done( function ( content, userinfo ) {
var parser, ast, parsedBlockReason;
self.setContent( content );
// check if user is blocked
if ( userinfo && userinfo.hasOwnProperty( 'blockid' ) ) {
// Workaround to parse a message parameter for mw.message, see T96885
// eslint-disable-next-line new-cap
parser = new mw.jqueryMsg.parser();
ast = parser.wikiTextToAst( userinfo.blockreason );
parsedBlockReason = parser.emitter.emit( ast );
toast.show(
mw.message(
'mobile-frontend-editor-blocked-info',
userinfo.blockedby,
parsedBlockReason
).parse()
);
self.hide();
}
self.clearSpinner();
} )
.fail( function () {
self.reportError( mw.msg( 'mobile-frontend-editor-error-loading' ) );
} );
},
/**
* Loads a {VisualEditorOverlay} and replaces the existing EditorOverlay with it
* based on the current option values.
* @method
* @private
* @param {Object} options Object passed to the constructor
*/
_switchToVisualEditor: function ( options ) {
var self = this;
this.log( {
action: 'abort',
type: 'switchnochange',
mechanism: 'navigate'
} );
// Save a user setting indicating that this user prefers using the VisualEditor
settings.save( 'preferredEditor', 'VisualEditor', true );
// Load the VisualEditor and replace the SourceEditor overlay with it
this.showSpinner();
this.$content.hide();
mw.loader.using(
'mobile.editor.ve',
function () {
var VisualEditorOverlay = M.require( 'mobile.editor.ve/VisualEditorOverlay' );
self.clearSpinner();
self.overlayManager.replaceCurrent( new VisualEditorOverlay( options ) );
},
function () {
self.clearSpinner();
self.$content.show();
// FIXME: We should show an error notification, but right now toast
// notifications are not dismissible when shown within the editor.
}
);
},
/**
* Reveals an abuse filter panel inside the view.
* @method
* @private
* @param {string} type The type of alert, e.g. 'warning' or 'disallow'
* @param {string} message Message to show in the AbuseFilter overlay
*/
_showAbuseFilter: function ( type, message ) {
this.abuseFilterPanel.show( type, message );
this.showHidden( '.save-header' );
// disable continue and save buttons, reenabled when user changes content
this.$( '.continue, .submit' ).prop( 'disabled', this.abuseFilterPanel.isDisallowed );
},
/**
* Executed when the editor clicks the save button. Handles logging and submitting
* the save action to the editor API.
* @inheritdoc
*/
onSaveBegin: function () {
var self = this,
options = {
summary: this.$( '.summary' ).val()
};
if ( self.sectionLine !== '' ) {
options.summary = '/* ' + self.sectionLine + ' */' + options.summary;
}
EditorOverlayBase.prototype.onSaveBegin.apply( this, arguments );
if ( this.confirmAborted ) {
return;
}
if ( this.captchaId ) {
options.captchaId = this.captchaId;
options.captchaWord = this.$( '.captcha-word' ).val();
}
this.showHidden( '.saving-header' );
this.gateway.save( options )
.done( function () {
var title = self.options.title;
// Special case behaviour of main page
if ( mw.config.get( 'wgIsMainPage' ) ) {
window.location = mw.util.getUrl( title );
return;
}
self.onSaveComplete();
} )
.fail( function ( data, code, response ) {
var msg,
// When save failed with one of these error codes, the returned
// message in response.error.info will be forwarded to the user.
// FIXME: This shouldn't be needed when info texts are all localized.
whitelistedErrorInfo = [
'readonly',
'blocked',
'autoblocked'
],
key = response && response.error && response.error.code,
typeMap = {
editconflict: 'editConflict',
wasdeleted: 'editPageDeleted',
'abusefilter-disallowed': 'extensionAbuseFilter',
captcha: 'extensionCaptcha',
spamprotectiontext: 'extensionSpamBlacklist',
'titleblacklist-forbidden-edit': 'extensionTitleBlacklist'
};
if ( data.type === 'captcha' ) {
self.captchaId = data.details.id;
self.handleCaptcha( data.details );
key = 'captcha';
} else if ( data.type === 'abusefilter' ) {
self._showAbuseFilter( data.details.type, data.details.message );
} else {
if ( key === 'editconflict' ) {
msg = mw.msg( 'mobile-frontend-editor-error-conflict' );
} else if ( $.inArray( key, whitelistedErrorInfo ) > -1 ) {
msg = response.error.info;
} else {
msg = mw.msg( 'mobile-frontend-editor-error' );
}
self.reportError( msg );
self.showHidden( '.save-header, .save-panel' );
}
self.log( {
action: 'saveFailure',
message: msg,
type: typeMap[key] || 'responseUnknown'
} );
} );
},
/**
* Checks whether the existing content has changed.
* @method
* @return {boolean}
*/
hasChanged: function () {
return this.gateway.hasChanged;
}
} );
M.define( 'mobile.editor.overlay/EditorOverlay', EditorOverlay );
}( mw.mobileFrontend, jQuery ) );