diff --git a/ApiVisualEditor.php b/ApiVisualEditor.php index 960edf55bf..a79d5ee90c 100644 --- a/ApiVisualEditor.php +++ b/ApiVisualEditor.php @@ -190,13 +190,15 @@ class ApiVisualEditor extends ApiBase { ); } - protected function diffWikitext( $title, $wikitext ) { + protected function diffWikitext( $title, $wikitext, $section = null ) { $apiParams = [ 'action' => 'query', 'prop' => 'revisions', 'titles' => $title->getPrefixedDBkey(), - 'rvdifftotext' => $this->pstWikitext( $title, $wikitext ) + 'rvdifftotext' => $this->pstWikitext( $title, $wikitext ), + 'rvsection' => $section ]; + $api = new ApiMain( new DerivativeRequest( $this->getRequest(), @@ -345,6 +347,11 @@ class ApiVisualEditor extends ApiBase { 'prop' => 'revisions', 'rvprop' => 'content' ]; + + if ( isset( $params['section'] ) ) { + $apiParams['rvsection'] = $params['section']; + } + $api = new ApiMain( new DerivativeRequest( $this->getRequest(), @@ -623,7 +630,8 @@ class ApiVisualEditor extends ApiBase { } } - $diff = $this->diffWikitext( $title, $wikitext ); + $section = isset( $params['section'] ) ? $params['section'] : null; + $diff = $this->diffWikitext( $title, $wikitext, $section ); if ( $diff['result'] === 'fail' ) { $this->dieUsage( 'Diff failed', 'difffailed' ); } @@ -759,6 +767,7 @@ class ApiVisualEditor extends ApiBase { ], ], 'wikitext' => null, + 'section' => null, 'oldid' => null, 'html' => null, 'etag' => null, diff --git a/ApiVisualEditorEdit.php b/ApiVisualEditorEdit.php index d7bee073b0..c52112fad1 100644 --- a/ApiVisualEditorEdit.php +++ b/ApiVisualEditorEdit.php @@ -252,6 +252,7 @@ class ApiVisualEditorEdit extends ApiVisualEditor { ApiBase::PARAM_REQUIRED => true, ], 'wikitext' => null, + 'section' => null, 'basetimestamp' => null, 'starttimestamp' => null, 'oldid' => null, diff --git a/modules/ve-mw/i18n/en.json b/modules/ve-mw/i18n/en.json index f58cb8156b..570d4cceb1 100644 --- a/modules/ve-mw/i18n/en.json +++ b/modules/ve-mw/i18n/en.json @@ -28,6 +28,7 @@ "apihelp-visualeditor-param-paction": "Action to perform.", "apihelp-visualeditor-param-page": "The page to perform actions on.", "apihelp-visualeditor-param-pst": "Pre-save transform wikitext before sending it to Parsoid (paction=parsefragment).", + "apihelp-visualeditor-param-section": "The section on which to act.", "apihelp-visualeditor-param-starttimestamp": "When saving, set this to the timestamp of when the page was loaded. Used to detect edit conflicts.", "apihelp-visualeditor-param-wikitext": "Wikitext to send to Parsoid to convert to HTML (paction=parsefragment).", "apihelp-visualeditoredit-description": "Save an HTML5 page to MediaWiki (converted to wikitext via the Parsoid service).", @@ -41,6 +42,7 @@ "apihelp-visualeditoredit-param-needcheck": "When saving, set this parameter if the revision might have roundtrip problems. This will result in the edit being tagged.", "apihelp-visualeditoredit-param-oldid": "The revision number to use. Defaults to latest revision. Use 0 for a new page.", "apihelp-visualeditoredit-param-page": "The page to perform actions on.", + "apihelp-visualeditoredit-param-section": "The section on which to act.", "apihelp-visualeditoredit-param-starttimestamp": "When saving, set this to the timestamp of when the page was loaded. Used to detect edit conflicts.", "apihelp-visualeditoredit-param-summary": "Edit summary.", "apihelp-visualeditoredit-param-watch": "", diff --git a/modules/ve-mw/i18n/qqq.json b/modules/ve-mw/i18n/qqq.json index 19c3fc2fe6..0ff1e0dd8b 100644 --- a/modules/ve-mw/i18n/qqq.json +++ b/modules/ve-mw/i18n/qqq.json @@ -41,6 +41,7 @@ "apihelp-visualeditor-param-paction": "{{doc-apihelp-param|visualeditor|paction}}", "apihelp-visualeditor-param-page": "{{doc-apihelp-param|visualeditor|page}}", "apihelp-visualeditor-param-pst": "{{doc-apihelp-param|visualeditor|pst}}", + "apihelp-visualeditor-param-section": "{{doc-apihelp-param|visualeditor|section}}", "apihelp-visualeditor-param-starttimestamp": "{{doc-apihelp-param|visualeditor|starttimestamp}}", "apihelp-visualeditor-param-wikitext": "{{doc-apihelp-param|visualeditor|wikitext}}", "apihelp-visualeditoredit-description": "{{doc-apihelp-description|visualeditoredit}}", @@ -54,6 +55,7 @@ "apihelp-visualeditoredit-param-needcheck": "{{doc-apihelp-param|visualeditoredit|needcheck}}", "apihelp-visualeditoredit-param-oldid": "{{doc-apihelp-param|visualeditoredit|oldid}}", "apihelp-visualeditoredit-param-page": "{{doc-apihelp-param|visualeditoredit|page}}", + "apihelp-visualeditoredit-param-section": "{{doc-apihelp-param|visualeditoredit|section}}", "apihelp-visualeditoredit-param-starttimestamp": "{{doc-apihelp-param|visualeditoredit|starttimestamp}}", "apihelp-visualeditoredit-param-summary": "{{doc-apihelp-param|visualeditoredit|summary}}\n{{Identical|Edit summary}}", "apihelp-visualeditoredit-param-watch": "{{doc-apihelp-param|visualeditoredit|watch}}", diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js index 41cfd7b346..d50c7d146c 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.init.js @@ -197,7 +197,7 @@ uri = veEditUri; } - activateTarget( mode, null, modified ); + activateTarget( mode, null, undefined, modified ); } } @@ -210,10 +210,11 @@ * * @private * @param {string} mode Target mode: 'visual' or 'source' + * @param {number} [section] Section to edit (currently just source mode) * @param {jQuery.Promise} [targetPromise] Promise that will be resolved with a ve.init.mw.DesktopArticleTarget * @param {boolean} [modified] The page was been modified before loading (e.g. in source mode) */ - function activateTarget( mode, targetPromise, modified ) { + function activateTarget( mode, section, targetPromise, modified ) { var dataPromise; // Only call requestPageData early if the target object isn't there yet. // If the target object is there, this is a second or subsequent load, and the @@ -226,6 +227,7 @@ return mw.libs.ve.targetLoader.requestPageData( mode, mw.config.get( 'wgRelevantPageName' ), + section, oldid, 'mwTarget', // ve.init.mw.DesktopArticleTarget.static.name modified @@ -459,10 +461,10 @@ $caVeEdit.remove(); } else if ( pageCanLoadVE ) { // Allow instant switching to edit mode, without refresh - $caVeEdit.on( 'click', init.onEditTabClick ); + $caVeEdit.on( 'click', init.onEditTabClick.bind( init, 'visual' ) ); } if ( conf.enableWikitext && mw.user.options.get( 'visualeditor-newwikitext' ) ) { - $caEdit.on( 'click', init.onEditSourceTabClick ); + $caEdit.on( 'click', init.onEditTabClick.bind( init, 'source' ) ); } // Alter the edit tab (#ca-edit) @@ -526,6 +528,7 @@ } ); } ) .addClass( 'mw-editsection-visualeditor' ); + if ( conf.tabPosition === 'before' ) { $editSourceLink.before( $editLink, $divider ); } else { @@ -543,8 +546,13 @@ // and would preserve the wrong DOM with a diff on top. $editsections .find( '.mw-editsection-visualeditor' ) - .on( 'click', init.onEditSectionLinkClick ) - ; + .on( 'click', init.onEditSectionLinkClick.bind( init, 'visual' ) ); + if ( conf.enableWikitext ) { + $editsections + // TOOD: Make this less fragile + .find( 'a:not( .mw-editsection-visualeditor )' ) + .on( 'click', init.onEditSectionLinkClick.bind( init, 'source' ) ); + } } }, @@ -562,7 +570,7 @@ return e && e.which && e.which === 1 && !( e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ); }, - onEditTabClick: function ( e ) { + onEditTabClick: function ( mode, e ) { if ( !init.isUnmodifiedLeftClick( e ) ) { return; } @@ -577,21 +585,10 @@ } } ); } else { - init.activateVe( 'visual' ); + init.activateVe( mode ); } }, - onEditSourceTabClick: function ( e ) { - if ( !init.isUnmodifiedLeftClick( e ) ) { - return; - } - e.preventDefault(); - if ( isLoading ) { - return; - } - init.activateVe( 'source' ); - }, - activateVe: function ( mode ) { var wikitext = $( '#wpTextbox1' ).textSelection( 'getContents' ); @@ -639,8 +636,8 @@ } }, - onEditSectionLinkClick: function ( e ) { - var targetPromise; + onEditSectionLinkClick: function ( mode, e ) { + var section, targetPromise; if ( !init.isUnmodifiedLeftClick( e ) ) { return; } @@ -661,11 +658,20 @@ history.pushState( { tag: 'visualeditor' }, document.title, this.href ); } - targetPromise = getTarget( 'visual' ).then( function ( target ) { - target.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) ); - return target; - } ); - activateTarget( 'visual', targetPromise ); + targetPromise = getTarget( mode ); + if ( mode === 'visual' ) { + targetPromise = targetPromise.then( function ( target ) { + target.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) ); + return target; + } ); + } else { + section = +( new mw.Uri( e.target.href ).query.section ); + targetPromise = targetPromise.then( function ( target ) { + target.section = section; + return target; + } ); + } + activateTarget( mode, section, targetPromise ); } }; @@ -765,6 +771,7 @@ $( function () { var showWikitextWelcome = true, + section = uri.query.vesection !== undefined ? uri.query.vesection : null, isLoggedIn = !mw.user.isAnon(), prefSaysShowWelcome = isLoggedIn && !mw.user.options.get( 'visualeditor-hidebetawelcome' ), urlSaysHideWelcome = 'hidewelcomedialog' in new mw.Uri( location.href ).query, @@ -838,7 +845,7 @@ ) { showWikitextWelcome = false; trackActivateStart( { - type: uri.query.vesection === undefined ? 'page' : 'section', + type: section === null ? 'page' : 'section', mechanism: 'url' } ); if ( isViewPage && uri.query.veaction in editModes ) { @@ -857,7 +864,7 @@ ) { action = 'editsource'; } - activateTarget( editModes[ action ] ); + activateTarget( editModes[ action ], section ); } } else if ( init.isVisualAvailable && 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 fe47c305da..4d4e4d2fa2 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopArticleTarget.js @@ -56,7 +56,7 @@ ve.init.mw.DesktopArticleTarget = function VeInitMwDesktopArticleTarget( config }; this.scrollTop = null; this.currentUri = currentUri; - this.section = currentUri.query.vesection; + this.section = currentUri.query.vesection !== undefined ? +currentUri.query.vesection : null; if ( $( '#wpSummary' ).length ) { this.initialEditSummary = $( '#wpSummary' ).val(); } else { diff --git a/modules/ve-mw/init/targets/ve.init.mw.DesktopWikitextArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.DesktopWikitextArticleTarget.js index b93a893ae7..4cdfad26f4 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.DesktopWikitextArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.DesktopWikitextArticleTarget.js @@ -66,19 +66,40 @@ ve.init.mw.DesktopWikitextArticleTarget.prototype.switchToWikitextEditor = funct * Switch to the visual editor. */ ve.init.mw.DesktopWikitextArticleTarget.prototype.switchToVisualEditor = function () { - var dataPromise; + var dataPromise, windowManager, switchWindow, + target = this; - dataPromise = mw.libs.ve.targetLoader.requestParsoidData( - this.pageName, - this.revid, - this.constructor.name, - this.edited, - this.getDocToSave() - ); + if ( this.section !== null ) { + // WT -> VE switching is not yet supported in sections, so + // show a discard-only confirm dialog, then reload the whole page. + windowManager = new OO.ui.WindowManager(); + switchWindow = new mw.libs.ve.SwitchConfirmDialog(); + $( 'body' ).append( windowManager.$element ); + windowManager.addWindows( [ switchWindow ] ); + windowManager.openWindow( switchWindow, { mode: 'simple' } ) + .then( function ( opened ) { + return opened; + } ) + .then( function ( closing ) { return closing; } ) + .then( function ( data ) { + if ( data && data.action === 'discard' ) { + target.setMode( 'visual' ); + target.reloadSurface(); + } + windowManager.destroy(); + } ); + } else { + dataPromise = mw.libs.ve.targetLoader.requestParsoidData( + this.pageName, + this.revid, + this.constructor.name, + this.edited, + this.getDocToSave() + ); - this.setMode( 'visual' ); - - this.reloadSurface( dataPromise ); + this.setMode( 'visual' ); + this.reloadSurface( dataPromise ); + } }; /** @@ -205,6 +226,16 @@ ve.init.mw.DesktopWikitextArticleTarget.prototype.createSurface = function ( dmD } }; +/** + * @inheritdoc + */ +ve.init.mw.DesktopWikitextArticleTarget.prototype.restoreEditSection = function () { + if ( this.mode !== 'source' ) { + // Parent method + return ve.init.mw.DesktopWikitextArticleTarget.super.prototype.restoreEditSection.apply( this, arguments ); + } +}; + /** * Get a wikitext fragment from a document * @@ -309,11 +340,17 @@ ve.init.mw.DesktopWikitextArticleTarget.prototype.createDocToSave = function () * @inheritdoc */ ve.init.mw.DesktopWikitextArticleTarget.prototype.tryWithPreparedCacheKey = function ( doc, options ) { + var data; if ( this.mode === 'source' ) { - return new mw.Api().post( ve.extendObject( {}, options, { - wikitext: doc, - format: 'json' - } ), + data = { + wikitext: doc, + format: 'json' + }; + if ( this.section !== null ) { + data.section = this.section; + } + return new mw.Api().post( + ve.extendObject( {}, options, data ), { contentType: 'multipart/form-data' } ); } else { diff --git a/modules/ve-mw/init/ve.init.MWVESwitchConfirmDialog.js b/modules/ve-mw/init/ve.init.MWVESwitchConfirmDialog.js index ce0fbd4928..6efa33cc0c 100644 --- a/modules/ve-mw/init/ve.init.MWVESwitchConfirmDialog.js +++ b/modules/ve-mw/init/ve.init.MWVESwitchConfirmDialog.js @@ -64,10 +64,12 @@ mw.libs.ve.SwitchConfirmDialog.static.actions = [ /** * @inheritdoc */ -mw.libs.ve.SwitchConfirmDialog.prototype.getSetupProcess = function () { +mw.libs.ve.SwitchConfirmDialog.prototype.getSetupProcess = function ( data ) { return mw.libs.ve.SwitchConfirmDialog.super.prototype.getSetupProcess.apply( this, arguments ) .next( function () { - if ( + if ( data && data.mode ) { + this.actions.setMode( data.mode ); + } else if ( mw.config.get( 'wgVisualEditorConfig' ).fullRestbaseUrl && !$( 'input[name=wpSection]' ).val() ) { diff --git a/modules/ve-mw/init/ve.init.mw.ArticleTarget.js b/modules/ve-mw/init/ve.init.mw.ArticleTarget.js index 545dba57c7..ab9fed569b 100644 --- a/modules/ve-mw/init/ve.init.mw.ArticleTarget.js +++ b/modules/ve-mw/init/ve.init.mw.ArticleTarget.js @@ -40,6 +40,7 @@ ve.init.mw.ArticleTarget = function VeInitMwArticleTarget( pageName, revisionId, this.pageExists = mw.config.get( 'wgRelevantArticleId', 0 ) !== 0; this.toolbarScrollOffset = mw.config.get( 'wgVisualEditorToolbarScrollOffset', 0 ); this.mode = config.mode || 'visual'; + this.section = null; // Sometimes we actually don't want to send a useful oldid // if we do, PostEdit will give us a 'page restored' message @@ -1022,6 +1023,7 @@ ve.init.mw.ArticleTarget.prototype.load = function ( dataPromise ) { this.loading = dataPromise || mw.libs.ve.targetLoader.requestPageData( this.mode, this.pageName, + this.section, this.requestedRevId, this.constructor.name ); @@ -1046,6 +1048,7 @@ ve.init.mw.ArticleTarget.prototype.clearState = function () { this.startTimeStamp = null; this.doc = null; this.originalHtml = null; + this.section = null; this.editNotices = []; this.remoteNotices = []; this.localNoticeMessages = []; @@ -1704,7 +1707,7 @@ ve.init.mw.ArticleTarget.prototype.getSaveDialogOpeningData = function () { ve.init.mw.ArticleTarget.prototype.restoreEditSection = function () { var surfaceView, $documentNode, $section, headingNode; - if ( this.section !== undefined && this.section > 0 ) { + if ( this.section !== null && this.section > 0 ) { surfaceView = this.getSurface().getView(); $documentNode = surfaceView.getDocument().getDocumentNode().$element; $section = $documentNode.find( 'h1, h2, h3, h4, h5, h6' ).eq( this.section - 1 ); @@ -1718,8 +1721,6 @@ ve.init.mw.ArticleTarget.prototype.restoreEditSection = function () { if ( headingNode ) { this.goToHeading( headingNode ); } - - this.section = undefined; } }; diff --git a/modules/ve-mw/init/ve.init.mw.ArticleTargetLoader.js b/modules/ve-mw/init/ve.init.mw.ArticleTargetLoader.js index 3df202d1ed..89b6f0a694 100644 --- a/modules/ve-mw/init/ve.init.mw.ArticleTargetLoader.js +++ b/modules/ve-mw/init/ve.init.mw.ArticleTargetLoader.js @@ -107,14 +107,15 @@ * * @param {string} mode Target mode: 'visual' or 'source' * @param {string} pageName Page name to request + * @param {number} [section] Section to edit (currently just source mode) * @param {string} [oldid] Old revision ID, current if omitted * @param {string} [targetName] Optional target name for tracking * @param {boolean} [modified] The page was been modified before loading (e.g. in source mode) * @return {jQuery.Promise} Abortable promise resolved with a JSON object */ - requestPageData: function ( mode, pageName, oldid, targetName, modified ) { + requestPageData: function ( mode, pageName, section, oldid, targetName, modified ) { if ( mode === 'source' ) { - return this.requestWikitext( pageName, oldid, targetName, modified ); + return this.requestWikitext( pageName, section, oldid, targetName, modified ); } else { return this.requestParsoidData( pageName, oldid, targetName, modified ); } @@ -245,7 +246,7 @@ return dataPromise; }, - requestWikitext: function ( pageName, oldid /*, targetName */ ) { + requestWikitext: function ( pageName, section, oldid /*, targetName */ ) { var data = { action: 'visualeditor', paction: 'wikitext', @@ -253,6 +254,11 @@ uselang: mw.config.get( 'wgUserLanguage' ) }; + // section should never really be undefined, but check just in case + if ( section !== null && section !== undefined ) { + data.section = section; + } + if ( oldid !== undefined ) { data.oldid = oldid; }