/* global $ */ /** * @class EditorOverlayBase * @private */ const 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 * @private */ 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 * @private * @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 ) { const 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( `
` ), /** * @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 ) { const self = this; this.saved = true; if ( newRevId ) { let 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 } ); if ( tempUserCreated && redirectUrl ) { // The caller handles this redirect, either in SourceEditorOverlay or in VE's ArticleTarget return; } 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 ) { let code = data && data.errors && data.errors[0] && data.errors[0].code; const // 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 ) { const errorNotice = new MessageBox( { type: '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 * @memberof EditorOverlayBase */ preRender: function () { const options = this.options; this.options.headers = [ headers.formHeader( util.template( ` {{^hasToolbar}} {{/hasToolbar}} {{#hasToolbar}}{{/hasToolbar}} {{#editSwitcher}}