Support only surfacing part of the document

Bug: T76541
Depends-On: I227a0d704b9b337cff2102d424be9795d6362ed7
Change-Id: Iac71a51c8696434658f24fbb41c8142237bd810e
This commit is contained in:
Ed Sanders 2019-02-13 13:21:26 +00:00 committed by Esanders
parent bc02c44d36
commit 3269d53632
14 changed files with 144 additions and 117 deletions

View file

@ -52,6 +52,7 @@
"VisualEditorEnableWikitextBetaFeature": false,
"VisualEditorEnableDiffPage": false,
"VisualEditorEnableDiffPageBetaFeature": false,
"VisualEditorEnableVisualSectionEditing": false,
"VisualEditorUseSingleEditTab": false,
"VisualEditorSingleEditTabSwitchTime": 20160101000000,
"VisualEditorTabPosition": "before",

View file

@ -899,6 +899,7 @@ class VisualEditorHooks {
'tabPosition' => $veConfig->get( 'VisualEditorTabPosition' ),
'tabMessages' => $veConfig->get( 'VisualEditorTabMessages' ),
'singleEditTab' => $veConfig->get( 'VisualEditorUseSingleEditTab' ),
'enableVisualSectionEditing' => $veConfig->get( 'VisualEditorEnableVisualSectionEditing' ),
'showBetaWelcome' => $veConfig->get( 'VisualEditorShowBetaWelcome' ),
'enableTocWidget' => $veConfig->get( 'VisualEditorEnableTocWidget' ),
'enableWikitext' => (

View file

@ -29,7 +29,7 @@ OO.inheritClass( ve.dm.MWHeadingNode, ve.dm.HeadingNode );
ve.dm.MWHeadingNode.static.name = 'mwHeading';
// Headings in wikitext only work in some contexts, they're impossible e.g. in list items
ve.dm.MWHeadingNode.static.suggestedParentNodeTypes = [ 'document', 'tableCell', 'div' ];
ve.dm.MWHeadingNode.static.suggestedParentNodeTypes = [ 'document', 'tableCell', 'div', 'section' ];
/* Registration */

View file

@ -29,7 +29,7 @@ OO.inheritClass( ve.dm.MWPreformattedNode, ve.dm.PreformattedNode );
ve.dm.MWPreformattedNode.static.name = 'mwPreformatted';
// Indent-pre in wikitext only works in some contexts, it's impossible e.g. in list items
ve.dm.MWPreformattedNode.static.suggestedParentNodeTypes = [ 'document', 'tableCell' ];
ve.dm.MWPreformattedNode.static.suggestedParentNodeTypes = [ 'document', 'tableCell', 'div', 'section' ];
/* Registration */

View file

@ -43,7 +43,7 @@ ve.dm.MWTableNode.static.classAttributes = {
// Tables in wikitext only work in some contexts, they're impossible e.g. in list items
ve.dm.MWTableNode.static.suggestedParentNodeTypes = [
'document', 'div', 'tableCell', 'tableCaption', 'mwImageCaption',
'document', 'div', 'tableCell', 'tableCaption', 'mwImageCaption', 'section',
// TODO: `paragraph` isn't really a suggested table parent. However,
// allowing it here interacts with our post-insertion cleanup for block
// nodes so that empty paragraphs get properly removed. We should find a

View file

@ -50,3 +50,13 @@
color: #999;
float: none;
}
/* Reset section node styles */
.ve-init-mw-target .ve-ce-sectionNode:before,
.ve-init-mw-target .ve-ce-sectionNode:after {
content: normal;
}
.ve-init-mw-target .ve-ce-surface-enabled .ve-ce-sectionNode:not( .ve-ce-activeNode-active ) {
opacity: 1;
}

View file

@ -182,8 +182,8 @@
function parseSection( section ) {
var parsedSection = section;
// Section must be a number, 'new' or 'T-' prefixed
if ( section !== 'new' && section.indexOf( 'T-' ) !== 0 ) {
parsedSection = +section;
if ( section !== 'new' ) {
parsedSection = section.indexOf( 'T-' ) === 0 ? +section.slice( 2 ) : +section;
if ( isNaN( parsedSection ) ) {
parsedSection = null;
}
@ -951,19 +951,11 @@
history.pushState( { tag: 'visualeditor' }, document.title, this.href );
}
if ( mode === 'visual' ) {
// Get section based on heading count (may differ from wikitext section count)
targetPromise = getTarget( mode ).then( function ( target ) {
target.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) );
return target;
} );
} else {
// Use section from URL
if ( section === undefined ) {
section = parseSection( uri.query.section );
}
targetPromise = getTarget( mode, section );
// Use section from URL
if ( section === undefined ) {
section = parseSection( uri.query.section );
}
targetPromise = getTarget( mode, section );
activateTarget( mode, section, targetPromise );
}
};

View file

@ -1138,7 +1138,7 @@ ve.init.mw.DesktopArticleTarget.prototype.getSaveDialogOpeningData = function ()
* Remember the window's scroll position.
*/
ve.init.mw.DesktopArticleTarget.prototype.saveScrollPosition = function () {
if ( this.getDefaultMode() === 'source' && this.section !== null ) {
if ( ( this.getDefaultMode() === 'source' || this.enableVisualSectionEditing ) && this.section !== null ) {
// Reset scroll to top if doing real section editing
this.scrollTop = 0;
} else {
@ -1306,7 +1306,7 @@ ve.init.mw.DesktopArticleTarget.prototype.updateHistoryState = function () {
* Page modifications for switching back to view mode.
*/
ve.init.mw.DesktopArticleTarget.prototype.restorePage = function () {
var uri, keys, section, $section;
var uri, keys, fragment, target;
// Skins like monobook don't have a tab for view mode and instead just have the namespace tab
// selected. We didn't deselect the namespace tab, so we're ready after deselecting #ca-ve-edit.
@ -1330,30 +1330,22 @@ ve.init.mw.DesktopArticleTarget.prototype.restorePage = function () {
if ( 'veaction' in uri.query ) {
delete uri.query.veaction;
}
if ( 'section' in uri.query ) {
if ( this.section !== null ) {
// Translate into a fragment for the new URI:
// This should be after replacePageContent if this is post-save, so we can just look
// at the headers on the page.
section = uri.query.section.toString().indexOf( 'T-' ) === 0 ? +uri.query.section.slice( 2 ) : uri.query.section;
$section = this.$editableContent.find( 'h1, h2, h3, h4, h5, h6' )
// Ignore headings inside TOC
.filter( function () {
return $( this ).closest( '#toc' ).length === 0;
} );
if ( section === 'new' ) {
// A new section is appended to the end, so take the last one.
section = $section.length;
}
$section = $section.eq( section - 1 ).find( '.mw-headline' );
fragment = this.getSectionFragmentFromPage();
if ( fragment ) {
uri.fragment = fragment;
this.viewUri.fragment = fragment;
target = document.getElementById( fragment );
if ( $section.length && $section.attr( 'id' ) ) {
uri.fragment = $section.attr( 'id' );
this.viewUri.fragment = uri.fragment;
// Scroll the page to the edited section
setTimeout( function () {
$section[ 0 ].scrollIntoView( true );
} );
if ( target ) {
// Scroll the page to the edited section
setTimeout( function () {
target.scrollIntoView( true );
} );
}
}
delete uri.query.section;
}
@ -1463,34 +1455,6 @@ ve.init.mw.DesktopArticleTarget.prototype.replacePageContent = function (
mw.libs.ve.setupEditLinks();
};
/**
* Get the numeric index of a section in the page.
*
* @method
* @param {HTMLElement} heading Heading element of section
*/
ve.init.mw.DesktopArticleTarget.prototype.getEditSection = function ( heading ) {
var $page = $( '#mw-content-text' ),
section = 0;
$page.find( 'h1, h2, h3, h4, h5, h6' ).not( '#toc h2' ).each( function () {
section++;
if ( this === heading ) {
return false;
}
} );
return section;
};
/**
* Store the section for which the edit link has been triggered.
*
* @method
* @param {HTMLElement} heading Heading element of section
*/
ve.init.mw.DesktopArticleTarget.prototype.saveEditSection = function ( heading ) {
this.section = this.getEditSection( heading );
};
/**
* Add onunload and onbeforeunload handlers.
*

View file

@ -19,6 +19,8 @@
* @param {Object} [config] Configuration options
*/
ve.init.mw.ArticleTarget = function VeInitMwArticleTarget( config ) {
var enableVisualSectionEditing;
config = config || {};
config.toolbarConfig = $.extend( {
shadow: true,
@ -38,6 +40,8 @@ ve.init.mw.ArticleTarget = function VeInitMwArticleTarget( config ) {
this.originalHtml = null;
this.toolbarSaveButton = null;
this.pageExists = mw.config.get( 'wgRelevantArticleId', 0 ) !== 0;
enableVisualSectionEditing = mw.config.get( 'wgVisualEditorConfig' ).enableVisualSectionEditing;
this.enableVisualSectionEditing = enableVisualSectionEditing === true || enableVisualSectionEditing === this.constructor.static.trackingName;
this.toolbarScrollOffset = mw.config.get( 'wgVisualEditorToolbarScrollOffset', 0 );
// A workaround, as default URI does not get updated after pushState (T74334)
this.currentUri = new mw.Uri( location.href );
@ -224,7 +228,7 @@ ve.init.mw.ArticleTarget.static.documentCommands = ve.init.mw.ArticleTarget.supe
/**
* @inheritdoc
*/
ve.init.mw.ArticleTarget.static.parseDocument = function ( documentString, mode, section ) {
ve.init.mw.ArticleTarget.static.parseDocument = function ( documentString, mode, section, onlySection ) {
// Add trailing linebreak to non-empty wikitext documents for consistency
// with old editor and usability. Will be stripped on save. T156609
if ( mode === 'source' && documentString ) {
@ -232,7 +236,7 @@ ve.init.mw.ArticleTarget.static.parseDocument = function ( documentString, mode,
}
// Parent method
return ve.init.mw.ArticleTarget.super.static.parseDocument.call( this, documentString, mode, section );
return ve.init.mw.ArticleTarget.super.static.parseDocument.call( this, documentString, mode, section, onlySection );
};
/**
@ -340,7 +344,8 @@ ve.init.mw.ArticleTarget.prototype.updateTabs = function ( editing ) {
* @param {string} status Text status message
*/
ve.init.mw.ArticleTarget.prototype.loadSuccess = function ( response ) {
var data = response ? ( response.visualeditor || response.visualeditoredit ) : null;
var mode, section,
data = response ? ( response.visualeditor || response.visualeditoredit ) : null;
if ( !data || typeof data.content !== 'string' ) {
this.loadFail( 've-api', 'No HTML content in response from server' );
@ -350,7 +355,9 @@ ve.init.mw.ArticleTarget.prototype.loadSuccess = function ( response ) {
this.etag = data.etag;
this.fromEditedState = !!data.fromEditedState;
this.switched = data.switched || 'wteswitched' in new mw.Uri( location.href ).query;
this.doc = this.constructor.static.parseDocument( this.originalHtml, this.getDefaultMode() );
mode = this.getDefaultMode();
section = ( mode === 'source' || this.enableVisualSectionEditing ) ? this.section : null;
this.doc = this.constructor.static.parseDocument( this.originalHtml, mode, section );
// Properties that don't come from the API
this.initialSourceRange = data.initialSourceRange;
@ -600,8 +607,8 @@ ve.init.mw.ArticleTarget.prototype.storeDocState = function ( html ) {
request: {
pageName: this.getPageName(),
mode: mode,
// Only source mode fetches data by section
section: mode === 'source' ? this.section : null
// Check true section editing is in use
section: ( mode === 'source' || this.enableVisualSectionEditing ) ? this.section : null
},
response: {
etag: this.etag,
@ -1024,7 +1031,6 @@ ve.init.mw.ArticleTarget.prototype.onSaveDialogPreview = function () {
if ( ve.getProp( response, 'visualeditor', 'result' ) === 'success' ) {
doc = target.constructor.static.parseDocument( response.visualeditor.content, 'visual' );
target.saveDialog.showPreview( doc, baseDoc );
} else {
target.saveDialog.showPreview(
ve.msg(
@ -2101,35 +2107,36 @@ ve.init.mw.ArticleTarget.prototype.getSaveDialogOpeningData = function () {
* @method
*/
ve.init.mw.ArticleTarget.prototype.restoreEditSection = function () {
var headingText,
section,
var dmDoc, headingModel, headingView, headingText,
section = this.section,
surface = this.getSurface(),
mode = surface.getMode(),
surfaceView, $documentNode, $section, headingNode;
mode = surface.getMode();
if ( this.section !== null && this.section !== 'new' && this.section !== 0 && this.section !== 'T-0' ) {
if ( section !== null && section !== 'new' && section !== 0 && section !== 'T-0' ) {
if ( mode === 'visual' ) {
// Get numerical part of section (strip 'T-'' if present)
section = this.section.toString().indexOf( 'T-' ) === 0 ? +this.section.slice( 2 ) : this.section;
surfaceView = surface.getView();
$documentNode = surfaceView.getDocument().getDocumentNode().$element;
// Find all headings including those inside templates, not just HeadingNodes
$section = $documentNode.find( 'h1, h2, h3, h4, h5, h6' )
// Ignore headings inside TOC
.filter( function () {
return $( this ).closest( '.ve-ui-mwTocWidget' ).length === 0;
} )
.eq( section - 1 );
headingNode = $section.data( 'view' );
if ( $section.length && new mw.Uri().query.summary === undefined ) {
// Due to interactions with Translate, strip out mw-
// editsection from the heading.
headingText = $section.clone().find( 'span.mw-editsection' ).remove().end().text();
}
if ( headingNode ) {
this.goToHeading( headingNode );
dmDoc = surface.getModel().getDocument();
// In ve.unwrapParsoidSections we copy the data-mw-section-id from the section element
// to the heading. Iterate over headings to find the one with the correct attribute
// in originalDomElements.
dmDoc.getNodesByType( 'mwHeading' ).some( function ( heading ) {
var domElements = heading.getOriginalDomElements( dmDoc.getStore() );
if (
domElements && domElements[ 0 ].nodeType === Node.ELEMENT_NODE &&
+domElements[ 0 ].getAttribute( 'data-mw-section-id' ) === section
) {
headingModel = heading;
return true;
}
return false;
} );
if ( headingModel ) {
headingView = surface.getView().getDocument().getDocumentNode().getNodeFromOffset( headingModel.getRange().start );
if ( new mw.Uri().query.summary === undefined ) {
headingText = headingView.$element.text();
}
if ( !this.enableVisualSectionEditing ) {
this.goToHeading( headingView );
}
}
} else if ( mode === 'source' ) {
// With elements of extractSectionTitle + stripSectionName TODO:
@ -2216,6 +2223,36 @@ ve.init.mw.ArticleTarget.prototype.scrollToHeading = function ( headingNode ) {
$window.scrollTop( headingNode.$element.offset().top - this.getToolbar().$element.height() );
};
/**
* Get the section ID's hash fragment using the page's PHP HTML
*
* TODO: Do this in a less skin-dependent way
*
* @param {number} section Section ID
* @return {string} Hash fragment, or null if not found
*/
ve.init.mw.ArticleTarget.prototype.getSectionFragmentFromPage = function () {
var section, $sections, $section;
// Assume there are section edit links, as the user just did a section edit. This also means
// that the section numbers line up correctly, as not every H_ tag is a numbered section.
$sections = $( '.mw-editsection' );
if ( this.section === 'new' ) {
// A new section is appended to the end, so take the last one.
section = $sections.length;
} else {
section = this.section;
}
if ( section > 0 ) {
$section = $sections.eq( section - 1 ).parent().find( '.mw-headline' );
if ( $section.length && $section.attr( 'id' ) ) {
return $section.attr( 'id' ) || '';
}
}
return '';
};
/**
* Show the beta dialog as needed
*/

View file

@ -108,7 +108,7 @@
* @param {string} pageName Page name to request
* @param {Object} [options] Options
* @param {boolean} [options.sessionStore] Store result in session storage (by page+mode+section) for auto-save
* @param {number|string} [options.section] Section to edit, number or 'new' (currently just source mode)
* @param {number|null|string} [options.section] Section to edit; number, null or 'new' (currently just source mode)
* @param {number} [options.oldId] Old revision ID. Current if omitted.
* @param {string} [options.targetName] Optional target name for tracking
* @param {boolean} [options.modified] The page was been modified before loading (e.g. in source mode)
@ -118,7 +118,7 @@
* @return {jQuery.Promise} Abortable promise resolved with a JSON object
*/
requestPageData: function ( mode, pageName, options ) {
var sessionState, request, dataPromise, apiRequest;
var sessionState, request, section, dataPromise, apiRequest, enableVisualSectionEditing;
options = options || {};
apiRequest = mode === 'source' ?
@ -133,12 +133,15 @@
if ( sessionState ) {
request = sessionState.request || {};
// Check true section editing is in use
enableVisualSectionEditing = mw.config.get( 'wgVisualEditorConfig' ).enableVisualSectionEditing;
section = request.mode === 'source' || enableVisualSectionEditing === true || enableVisualSectionEditing === options.targetName ?
options.section : null;
// Check the requested page, mode and section match the stored one
if (
request.pageName === pageName &&
request.mode === mode &&
// Only check sections in source mode
( request.mode !== 'source' || request.section === options.section )
request.section === section
// NB we don't cache by oldid so that cached results can be recovered
// even if the page has been since edited
) {

View file

@ -31,7 +31,7 @@
metadataIdRegExp = ve.init.platform.getMetadataIdRegExp(),
data = response ? ( response.visualeditor || response.visualeditoredit ) : null;
if ( data && typeof data.content === 'string' ) {
doc = targetClass.static.parseDocument( data.content, 'visual', section );
doc = targetClass.static.parseDocument( data.content, 'visual', section, true );
// Strip RESTBase IDs
Array.prototype.forEach.call( doc.querySelectorAll( '[id^="mw"]' ), function ( element ) {
if ( element.id.match( metadataIdRegExp ) ) {

View file

@ -178,9 +178,11 @@ ve.init.mw.Target.prototype.createModelFromDom = function () {
/**
* @inheritdoc
* @param {number} [section] Section
* @param {number|string|null} section Section
* @param {boolean} [onlySection] Only return the requested section, otherwise returns the
* whole document with just the requested section still wrapped (visual mode only).
*/
ve.init.mw.Target.static.parseDocument = function ( documentString, mode, section ) {
ve.init.mw.Target.static.parseDocument = function ( documentString, mode, section, onlySection ) {
var doc, sectionNode;
if ( mode === 'source' ) {
// Parent method
@ -189,14 +191,17 @@ ve.init.mw.Target.static.parseDocument = function ( documentString, mode, sectio
// Parsoid documents are XHTML so we can use parseXhtml which fixed some IE issues.
doc = ve.parseXhtml( documentString );
if ( section !== undefined ) {
sectionNode = doc.body.querySelector( '[data-mw-section-id="' + section + '"]' );
doc.body.innerHTML = '';
if ( sectionNode ) {
doc.body.appendChild( sectionNode );
if ( onlySection ) {
sectionNode = doc.body.querySelector( '[data-mw-section-id="' + section + '"]' );
doc.body.innerHTML = '';
if ( sectionNode ) {
doc.body.appendChild( sectionNode );
}
} else {
// Strip Parsoid sections
ve.unwrapParsoidSections( doc.body, section );
}
}
// Strip Parsoid sections
ve.unwrapParsoidSections( doc.body );
// Strip legacy IDs, for example in section headings
ve.stripParsoidFallbackIds( doc.body );
// Fix relative or missing base URL if needed
@ -275,6 +280,11 @@ ve.init.mw.Target.prototype.getHtml = function ( newDoc, oldDoc ) {
'div.donut-container' // Web of Trust (T189148)
].join( ',' ) )
.remove();
// data-mw-section-id is copied to headings by ve.unwrapParsoidSections
// Remove these to avoid triggering selser.
$( newDoc ).find( '[data-mw-section-id]:not( section )' ).removeAttr( 'data-mw-section-id' );
// Add doctype manually
return '<!doctype html>' + ve.serializeXhtml( newDoc );
};

View file

@ -171,9 +171,9 @@ ve.ui.MWTocWidget.prototype.build = function () {
return $list.children( 'li' ).length + ( n === stack.length - 1 ? 1 : 0 );
}
function linkClickHandler( heading ) {
function linkClickHandler( /* heading */ ) {
surfaceView.focus();
ve.init.target.goToHeading( heading );
// TODO: Impement heading scroll
return false;
}

View file

@ -34,10 +34,19 @@ ve.decodeURIComponentIntoArticleTitle = function ( s, preserveUnderscores ) {
* Unwrap Parsoid sections
*
* @param {HTMLElement} element Parent element, e.g. document body
* @param {number} [keepSection] Section to keep
*/
ve.unwrapParsoidSections = function ( element ) {
ve.unwrapParsoidSections = function ( element, keepSection ) {
Array.prototype.forEach.call( element.querySelectorAll( 'section[data-mw-section-id]' ), function ( section ) {
var parent = section.parentNode;
var parent = section.parentNode,
sectionId = section.getAttribute( 'data-mw-section-id' );
// Copy section ID to first child (should be a heading)
if ( sectionId > 0 ) {
section.firstChild.setAttribute( 'data-mw-section-id', sectionId );
}
if ( keepSection !== undefined && +sectionId === keepSection ) {
return;
}
while ( section.firstChild ) {
parent.insertBefore( section.firstChild, section );
}