Source: mobile.editor.overlay/EditorOverlayBase.js

/* global $ */
var Overlay = require( '../mobile.startup/Overlay' ),
	util = require( '../mobile.startup/util' ),
	parseBlockInfo = require( './parseBlockInfo' ),
	headers = require( '../mobile.startup/headers' ),
	icons = require( '../mobile.startup/icons' ),
	Button = require( '../mobile.startup/Button' ),
	IconButton = require( '../mobile.startup/IconButton' ),
	mfExtend = require( '../mobile.startup/mfExtend' ),
	blockMessageDrawer = require( './blockMessageDrawer' ),
	MessageBox = require( '../mobile.startup/MessageBox' ),
	mwUser = mw.user;

/**
 * 'Edit' button
 *
 * @param {OO.ui.ToolGroup} toolGroup
 * @param {Object} config
 */
function EditVeTool( toolGroup, config ) {
	config = config || {};
	config.classes = [ 'visual-editor' ];
	EditVeTool.super.call( this, toolGroup, config );
}
OO.inheritClass( EditVeTool, OO.ui.Tool );

EditVeTool.static.name = 'editVe';
EditVeTool.static.icon = 'edit';
EditVeTool.static.group = 'editorSwitcher';
EditVeTool.static.title = mw.msg( 'mobile-frontend-editor-switch-visual-editor' );
/**
 * click handler
 *
 * @memberof EditVeTool
 * @instance
 */
EditVeTool.prototype.onSelect = function () {
	// will be overridden later
};
/**
 * Toolbar update state handler.
 *
 * @memberof EditVeTool
 * @instance
 */
EditVeTool.prototype.onUpdateState = function () {
	// do nothing
};

/**
 * Base class for SourceEditorOverlay and VisualEditorOverlay
 *
 * @class EditorOverlayBase
 * @extends Overlay
 * @uses IconButton
 * @uses user
 * @param {Object} params Configuration options
 * @param {boolean} params.editSwitcher whether possible to switch mode in header
 * @param {boolean} params.hasToolbar whether the editor has a toolbar
 */
function EditorOverlayBase( params ) {
	var
		options = util.extend(
			true,
			{
				onBeforeExit: this.onBeforeExit.bind( this ),
				className: 'overlay editor-overlay',
				isBorderBox: false
			},
			params,
			{
				events: util.extend(
					{
						'click .back': 'onClickBack',
						'click .continue': 'onClickContinue',
						'click .submit': 'onClickSubmit',
						'click .anonymous': 'onClickAnonymous'
					},
					params.events
				)
			}
		);

	if ( options.isNewPage ) {
		options.placeholder = mw.msg( 'mobile-frontend-editor-placeholder-new-page', mwUser );
	}
	// change the message to request a summary when not in article namespace
	if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 ) {
		options.summaryRequestMsg = mw.msg( 'mobile-frontend-editor-summary' );
	}
	this.isNewPage = options.isNewPage;
	this.sectionId = options.sectionId;
	this.overlayManager = options.overlayManager;

	Overlay.call( this, options );
}

mfExtend( EditorOverlayBase, Overlay, {
	/**
	 * @memberof EditorOverlayBase
	 * @instance
	 * @mixes Overlay#defaults
	 * @property {Object} defaults Default options hash.
	 * @property {OverlayManager} defaults.overlayManager instance
	 * @property {mw.Api} defaults.api to interact with
	 * @property {boolean} defaults.hasToolbar Whether the editor has a toolbar or not. When
	 *  disabled a header will be show instead.
	 * @property {string} defaults.continueMsg Caption for the next button on edit form
	 * which takes you to the screen that shows a preview and license information.
	 * @property {string} defaults.closeMsg Caption for a button that takes you back to editing
	 * from edit preview screen.
	 * @property {string} defaults.summaryRequestMsg Header above edit summary input field
	 * asking the user to summarize the changes they made to the page.
	 * @property {string} defaults.summaryMsg A placeholder with examples for the summary input
	 * field asking user what they changed.
	 * @property {string} defaults.placeholder Placeholder text for empty sections.
	 * @property {string} defaults.captchaMsg Placeholder for captcha input field.
	 * @property {string} defaults.captchaTryAgainMsg A message shown when user enters
	 * wrong CAPTCHA and a new one is displayed.
	 * @property {string} defaults.switchMsg Label for button that allows the user
	 * to switch between two different editing interfaces.
	 * @property {string} defaults.licenseMsg Text and link of the license,
	 * under which this contribution will be released to inform the user.
	 */
	defaults: util.extend( {}, Overlay.prototype.defaults, {
		hasToolbar: false,
		continueMsg: mw.msg( 'mobile-frontend-editor-continue' ),
		closeMsg: mw.msg( 'mobile-frontend-editor-keep-editing' ),
		summaryRequestMsg: mw.msg( 'mobile-frontend-editor-summary-request' ),
		summaryMsg: mw.msg( 'mobile-frontend-editor-summary-placeholder' ),
		placeholder: mw.msg( 'mobile-frontend-editor-placeholder' ),
		captchaMsg: mw.msg( 'mobile-frontend-account-create-captcha-placeholder' ),
		captchaTryAgainMsg: mw.msg( 'mobile-frontend-editor-captcha-try-again' ),
		switchMsg: mw.msg( 'mobile-frontend-editor-switch-editor' ),
		confirmMsg: mw.msg( 'mobile-frontend-editor-cancel-confirm' ),
		licenseMsg: undefined
	} ),
	/**
	 * @inheritdoc
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	template: util.template( `
<div class="overlay-header-container header-container position-fixed"></div>

<div class="overlay-content">
	<div class="panels">
		<div class="save-panel panel hideable hidden">
			<div id="error-notice-container"></div>
			<h2 class="summary-request">{{{summaryRequestMsg}}}</h2>
			<div class="summary-input"></div>
			{{#licenseMsg}}<div class="license">{{{licenseMsg}}}</div>{{/licenseMsg}}
		</div>
		<div class="captcha-panel panel hideable hidden">
			<div class="captcha-box">
				<img id="image" src="">
				<div id="question"></div>
				<div class="cdx-text-input">
					<input class="captcha-word cdx-text-input__input" placeholder="{{captchaMsg}}" />
				</div>
			</div>
		</div>
	</div>
	{{>content}}
</div>
<div class="overlay-footer-container position-fixed">
	{{>footer}}
</div>
	` ),
	/**
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	sectionId: '',
	/**
	 * Logs an event to http://meta.wikimedia.org/wiki/Schema:EditAttemptStep
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Object} data
	 */
	log: function ( data ) {
		mw.track( 'editAttemptStep', util.extend( data, {
			// eslint-disable-next-line camelcase
			editor_interface: this.editor
		} ) );
	},
	/**
	 * Logs an event to http://meta.wikimedia.org/wiki/Schema:VisualEditorFeatureUse
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Object} data
	 */
	logFeatureUse: function ( data ) {
		mw.track( 'visualEditorFeatureUse', util.extend( data, {
			// eslint-disable-next-line camelcase
			editor_interface: this.editor
		} ) );
	},

	/**
	 * If this is a new article, require confirmation before saving.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @return {boolean} The user confirmed saving
	 */
	confirmSave: function () {
		if ( this.isNewPage &&
			// TODO: Replace with an OOUI dialog
			// eslint-disable-next-line no-alert
			!window.confirm( mw.msg( 'mobile-frontend-editor-new-page-confirm', mwUser ) )
		) {
			return false;
		} else {
			return true;
		}
	},
	/**
	 * Executed when page save is complete. Updates urls and shows toast message.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {number|null} newRevId ID of the newly created revision, or null if it was a null edit.
	 * @param {string} [redirectUrl] URL to redirect to, if different than the current URL.
	 * @param {boolean} [tempUserCreated] Whether a temporary user was created
	 */
	onSaveComplete: function ( newRevId, redirectUrl, tempUserCreated ) {
		var
			self = this;

		this.saved = true;

		if ( newRevId ) {
			var action;
			if ( self.isNewPage ) {
				action = 'created';
			} else if ( self.options.oldId ) {
				action = 'restored';
			} else {
				action = 'saved';
			}
			self.showSaveCompleteMsg( action, tempUserCreated );
		}

		// Ensure we don't lose this event when logging
		this.log( {
			action: 'saveSuccess',
			// eslint-disable-next-line camelcase
			revision_id: newRevId
		} );
		setTimeout( function () {
			// Wait for any other teardown navigation to happen (e.g. router.back())
			// before setting our final location.
			if ( redirectUrl ) {
				// eslint-disable-next-line no-restricted-properties
				window.location.href = redirectUrl;
			} else if ( self.sectionId ) {
				// Ideally we'd want to do this via replaceState (see T189173)
				// eslint-disable-next-line no-restricted-properties
				window.location.hash = '#' + self.sectionId;
			} else {
				// Cancel the hash fragment
				// otherwise clicking back after a save will take you back to the editor.
				// We avoid calling the hide method of the overlay here as this can be asynchronous
				// and may conflict with the window.reload call below.
				// eslint-disable-next-line no-restricted-properties
				window.location.hash = '#';
			}
		} );
	},
	/**
	 * Show a save-complete message to the user
	 *
	 * @inheritdoc
	 * @memberof VisualEditorOverlay
	 * @instance
	 * @param {string} action One of 'saved', 'created', 'restored'
	 * @param {boolean} [tempUserCreated] Whether a temporary user was created
	 */
	showSaveCompleteMsg: function ( action, tempUserCreated ) {
		__non_webpack_require__( 'mediawiki.action.view.postEdit' ).fireHook( action, tempUserCreated );
	},
	/**
	 * Executed when page save fails. Handles logging the error. Subclasses
	 * should display error messages as appropriate.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Object} data API response
	 */
	onSaveFailure: function ( data ) {
		var code = data && data.errors && data.errors[0] && data.errors[0].code,
			// Compare to ve.init.mw.ArticleTargetEvents.js in VisualEditor.
			typeMap = {
				badtoken: 'userBadToken',
				assertanonfailed: 'userNewUser',
				assertuserfailed: 'userNewUser',
				assertnameduserfailed: 'userNewUser',
				'abusefilter-disallowed': 'extensionAbuseFilter',
				'abusefilter-warning': 'extensionAbuseFilter',
				captcha: 'extensionCaptcha',
				// FIXME: This language is non-inclusive and we would love to change it,
				// but this relates to an error code provided by software.
				// This is blocked on T254649
				spamblacklist: 'extensionSpamBlacklist',
				// FIXME: This language is non-inclusive and we would love to change it,
				// but this relates to an error code provided by software.
				// Removal of this line is blocked on T254650.
				'titleblacklist-forbidden': 'extensionTitleBlacklist',
				pagedeleted: 'editPageDeleted',
				editconflict: 'editConflict'
			};

		if ( data.edit && data.edit.captcha ) {
			code = 'captcha';
		}

		this.log( {
			action: 'saveFailure',
			message: code,
			type: typeMap[code] || 'responseUnknown'
		} );
	},
	/**
	 * Report load errors back to the user. Silently record the error using EventLogging.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {string} text Text (HTML) of message to display to user
	 */
	reportError: function ( text ) {
		var errorNotice = new MessageBox( {
			className: 'mw-message-box-error',
			msg: text,
			heading: mw.msg( 'mobile-frontend-editor-error' )
		} );
		this.$errorNoticeContainer.html( errorNotice.$el );
	},
	hideErrorNotice: function () {
		this.$errorNoticeContainer.empty();
	},
	/**
	 * Prepares the penultimate screen before saving.
	 * Expects to be overridden by child class.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onStageChanges: function () {
		this.showHidden( '.save-header, .save-panel' );
		this.hideErrorNotice();
		this.log( {
			action: 'saveIntent'
		} );
		// Scroll to the top of the page, so that the summary input is visible
		// (even if overlay was scrolled down when editing) and weird iOS header
		// problems are avoided (header position not updating to the top of the
		// screen, instead staying lower until a subsequent scroll event).
		window.scrollTo( 0, 1 );
	},
	/**
	 * Executed when the editor clicks the save button. Expects to be overridden by child
	 * class. Checks if the save needs to be confirmed.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onSaveBegin: function () {
		this.confirmAborted = false;
		this.hideErrorNotice();
		// Ask for confirmation in some cases
		if ( !this.confirmSave() ) {
			this.confirmAborted = true;
			return;
		}
		this.log( {
			action: 'saveAttempt'
		} );
	},
	/**
	 * @inheritdoc
	 */
	preRender: function () {
		const options = this.options;

		this.options.headers = [
			headers.formHeader(
				util.template( `
{{^hasToolbar}}
<div class="overlay-title">
	<h2>{{{editingMsg}}}</h2>
</div>
{{/hasToolbar}}
{{#hasToolbar}}<div class="toolbar"></div>{{/hasToolbar}}
{{#editSwitcher}}
	<div class="switcher-container notheme">
	</div>
{{/editSwitcher}}
				` ).render( {
					hasToolbar: options.hasToolbar,
					editSwitcher: options.editSwitcher,
					editingMsg: options.editingMsg
				} ),
				options.readOnly ? [] : [
					new IconButton( {
						tagName: 'button',
						action: 'progressive',
						weight: 'primary',
						icon: 'next-invert',
						additionalClassNames: 'continue',
						disabled: true,
						title: options.continueMsg
					} )
				],
				icons.cancel(),
				'initial-header'
			),
			headers.saveHeader( options.previewingMsg, 'save-header hidden' ),
			headers.savingHeader( mw.msg( 'mobile-frontend-editor-wait' ) )
		];
	},

	/**
	 * @inheritdoc
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	postRender: function () {
		this.$errorNoticeContainer = this.$el.find( '#error-notice-container' );

		Overlay.prototype.postRender.apply( this );
		this.showHidden( '.initial-header' );
	},
	show: function () {
		var self = this;
		this.allowCloseWindow = mw.confirmCloseWindow( {
			// Returns true if content has changed
			test: function () {
				// Check if content has changed
				return self.hasChanged();
			},

			// Message to show the user, if content has changed
			message: mw.msg( 'mobile-frontend-editor-cancel-confirm' ),
			// Event namespace
			namespace: 'editwarning'
		} );

		this.saved = false;
		Overlay.prototype.show.call( this );

		// Inform other interested code that the editor has loaded
		/**
		 * @event mobileFrontend.editorOpened
		 * @internal for use in ContentTranslation and GrowthExperiments only.
		 */
		mw.hook( 'mobileFrontend.editorOpened' ).fire( this.editor );
	},
	/**
	 * Back button click handler
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onClickBack: function () {},
	/**
	 * Submit button click handler
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onClickSubmit: function () {
		this.onSaveBegin();
	},
	/**
	 * Continue button click handler
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onClickContinue: function () {
		this.onStageChanges();
	},
	/**
	 * "Edit without logging in" button click handler
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	onClickAnonymous: function () {},
	/**
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Function} exit Callback to exit the overlay
	 * @param {Function} cancel Callback to cancel exiting the overlay
	 */
	onBeforeExit: function ( exit, cancel ) {
		var self = this;
		if ( this.hasChanged() && !this.switching ) {
			if ( !this.windowManager ) {
				this.windowManager = OO.ui.getWindowManager();
				this.windowManager.addWindows( [ new mw.widgets.AbandonEditDialog() ] );
			}
			this.windowManager.openWindow( 'abandonedit' )
				.closed.then( function ( data ) {
					if ( data && data.action === 'discard' ) {
						// log abandonment
						self.log( {
							action: 'abort',
							mechanism: 'cancel',
							type: 'abandon'
						} );
						self.onExit();
						exit();
					}
				} );
			cancel();
			return;
		}
		if ( !this.switching && !this.saved ) {
			// log leaving without changes
			this.log( {
				action: 'abort',
				mechanism: 'cancel',
				// if this is VE, hasChanged will be false because the Surface has
				// already been destroyed (which is good because it stops us
				// double-showing the abandon changes dialog above)... but we can
				// test whether there *were* changes for logging purposes by
				// examining the target:
				type: ( this.target && this.target.edited ) ? 'abandon' : 'nochange'
			} );
		}
		this.onExit();
		exit();
		if ( mw.config.get( 'wgAction' ) === 'edit' ) {
			// We got into the overlay via directly visiting an action=edit
			// URL, which has been taken over. As such, depending on
			// how we got here, the normal overlay process isn't going to
			// produce the correct result.
			setTimeout( function () {
				// This needs to happen after the overlay-hide has completed
				// so we have access to the "real" URL, and `exit`
				// unfortunately doesn't expose a promise for this. There's
				// several setTimeouts within the hide, so we're just going
				// to use a long-enough setTimeout of our own to skip those.
				if ( !mw.util.getParamValue( 'veaction' ) && !mw.util.getParamValue( 'action' ) ) {
					// Use reload if possible, to emulate having gone back
					location.reload();
				} else {
					location.href = mw.util.getUrl();
				}
			}, 100 );
		}
	},
	onExit: function () {
		// May not be set if overlay has not been previously shown
		if ( this.allowCloseWindow ) {
			this.allowCloseWindow.release();
		}
		/**
		 * @event mobileFrontend.editorClosed
		 * @internal for use in ContentTranslation and GrowthExperiments only.
		 */
		mw.hook( 'mobileFrontend.editorClosed' ).fire();
	},
	/**
	 * Sets additional values used for anonymous editing warning.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Object} options
	 * @return {jQuery.Element}
	 */
	createAnonWarning: function ( options ) {
		var $actions = $( '<div>' ).addClass( 'actions' ),
			// Use MediaWiki ResourceLoader require(), not Webpack require()
			contLangMessages = (
				// eslint-disable-next-line camelcase
				__non_webpack_require__( './contLangMessages.json' )
			),
			msg = this.gateway.wouldautocreate ?
				'mobile-frontend-editor-autocreatewarning' :
				'mobile-frontend-editor-anonwarning',
			$anonWarning = $( '<div>' ).addClass( 'anonwarning content' ).append(
				new MessageBox( {
					className: 'mw-message-box-notice anon-msg',
					// eslint-disable-next-line mediawiki/msg-doc
					msg: mw.message( msg, contLangMessages[ 'tempuser-helppage' ] ).parse()
				} ).$el,
				$actions
			),
			params = util.extend( {
				returnto: options.returnTo || (
					// use wgPageName as this includes the namespace if outside Main
					mw.config.get( 'wgPageName' ) + '#/editor/' + ( options.sectionId || 'all' )
				),
				warning: 'mobile-frontend-edit-login-action'
			}, options.queryParams ),
			signupParams = util.extend( {
				type: 'signup',
				warning: 'mobile-frontend-edit-signup-action'
			}, options.signupQueryParams ),
			anonymousEditorActions = [
				new Button( {
					label: mw.msg( 'mobile-frontend-editor-anon' ),
					block: true,
					additionalClassNames: 'anonymous progressive',
					progressive: true
				} ),
				new Button( {
					block: true,
					href: mw.util.getUrl( 'Special:UserLogin', params ),
					label: mw.msg( 'mobile-frontend-watchlist-cta-button-login' )
				} ),
				new Button( {
					block: true,
					href: mw.util.getUrl( 'Special:UserLogin', util.extend( params, signupParams ) ),
					label: mw.msg( 'mobile-frontend-watchlist-cta-button-signup' )
				} )
			];

		$actions.append(
			anonymousEditorActions.map( function ( action ) {
				return action.$el;
			} )
		);

		return $anonWarning;
	},
	/**
	 * Creates and returns a copy of the anon talk message warning
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @return {jQuery.Element}
	 */
	createAnonTalkWarning: function () {
		return $( '.minerva-anon-talk-message' ).clone();
	},
	/**
	 * Get an options object not containing any defaults or editor
	 * specific options, so that it can be used to construct a
	 * different editor for switching.
	 *
	 * @return {Object} Options
	 */
	getOptionsForSwitch: function () {
		// Only preserve options that would be passed in editor.js#setupEditor
		// and skip over defaults.
		return {
			switched: true,
			overlayManager: this.options.overlayManager,
			currentPageHTMLParser: this.options.currentPageHTMLParser,
			fakeScroll: this.options.fakeScroll,
			api: this.options.api,
			licenseMsg: this.options.licenseMsg,
			title: this.options.title,
			titleObj: this.options.titleObj,
			isAnon: this.options.isAnon,
			isNewPage: this.options.isNewPage,
			oldId: this.options.oldId,
			contentLang: this.options.contentLang,
			contentDir: this.options.contentDir,
			sectionId: this.options.sectionId
		};
	},

	/**
	 * Checks whether the state of the thing being edited as changed. Expects to be
	 * implemented by child class.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 */
	hasChanged: function () {},
	/**
	 * Get a promise that is resolved when the editor data has loaded,
	 * or rejected when we're refusing to load the editor because the user is blocked.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @return {jQuery.Promise}
	 */
	getLoadingPromise: function () {
		return this.dataPromise.then( function ( result ) {
			// check if user is blocked
			if ( result && result.blockinfo ) {
				var block = parseBlockInfo( result.blockinfo ),
					message = blockMessageDrawer( block );
				return util.Deferred().reject( message );
			}
			return result;
		} );
	},
	showEditNotices: function () {
		var overlay = this;
		if ( mw.config.get( 'wgMFEditNoticesFeatureConflict' ) ) {
			return;
		}
		this.getLoadingPromise().then( function ( data ) {
			if ( data.notices ) {
				var editNotices = Object.keys( data.notices ).filter( function ( key ) {
					if ( key.indexOf( 'editnotice' ) !== 0 ) {
						return false;
					}
					if ( key === 'editnotice-notext' ) {
						// This notice is shown on pages which don't have any
						// other edit notices. It's blank by default, but
						// some wikis have it template-generated and hidden
						// by CSS. It's filtered out from VE's API response,
						// but not from the source mode.
						return false;
					}
					// The contents of an edit notice is unlikely to change in the 24 hour
					// expiry window, so just record that a notice with this key has been shown.
					// If a cheap hashing function was available in core (or the API provided
					// as hash) it could be used here instead.
					var storageKey = 'mf-editnotices/' + mw.config.get( 'wgPageName' ) + '#' + key;
					if ( mw.storage.get( storageKey ) ) {
						return false;
					}
					mw.storage.set( storageKey, '1', 24 * 60 * 60 );
					return true;
				} );

				if ( editNotices.length ) {
					mw.loader.using( 'oojs-ui-windows' ).then( function () {
						var $container = $( '<div>' ).addClass( 'editor-overlay-editNotices' );
						editNotices.forEach( function ( key ) {
							var $notice = $( '<div>' ).append( data.notices[ key ] );
							$notice.addClass( 'editor-overlay-editNotice' );
							$container.append( $notice );
						} );
						OO.ui.alert( $container );

						overlay.logFeatureUse( {
							feature: 'notices',
							action: 'show'
						} );
					} );
				}
			}
		} );
	},
	/**
	 * Handles a failed save due to a CAPTCHA provided by ConfirmEdit extension.
	 *
	 * @memberof EditorOverlayBase
	 * @instance
	 * @param {Object} details Details returned from the api.
	 */
	handleCaptcha: function ( details ) {
		var self = this,
			$input = this.$el.find( '.captcha-word' );

		if ( this.captchaShown ) {
			$input.val( '' );
			$input.attr( 'placeholder', this.options.captchaTryAgainMsg );
			setTimeout( function () {
				$input.attr( 'placeholder', self.options.captchaMsg );
			}, 2000 );
		}

		// handle different mime types different
		if ( details.mime.indexOf( 'image/' ) === 0 ) {
			// image based CAPTCHA's like provided by FancyCaptcha, ReCaptcha or similar
			this.$el.find( '.captcha-panel#question' ).detach();
			this.$el.find( '.captcha-panel img' ).attr( 'src', details.url );
		} else {
			// not image based CAPTCHA.
			this.$el.find( '.captcha-panel #image' ).detach();
			if ( details.mime.indexOf( 'text/html' ) === 0 ) {
				// handle mime type of HTML as HTML content (display as-is).
				// QuestyCaptcha now have default MIME type "text/html": see T147606
				this.$el.find( '.captcha-panel #question' ).html( details.question );
			} else {
				// handle mime types
				// (other than image based ones and HTML based ones)
				// as plain text by default.
				// e.g. MathCaptcha (solve a math formula) or
				// SimpleCaptcha (simple math formula)
				this.$el.find( '.captcha-panel #question' ).text( details.question );
			}
		}

		this.showHidden( '.save-header, .captcha-panel' );
		this.captchaShown = true;
	}
} );

module.exports = EditorOverlayBase;