Pass visibleSection & visibleSectionOffset to target

* Find the first section below the top of the viewport
  (usually visible) and measure its offset.
* After loading the editor, ensure this heading is still
  at the same position on the page.

Bug: T296910
Change-Id: I9a05ea74ba3c19a4a91ddc1bc0afe311851c53e6
This commit is contained in:
Ed Sanders 2021-12-21 18:48:27 +00:00 committed by Bartosz Dziewoński
parent e1b9e6a98e
commit c0f3fc3a78
4 changed files with 124 additions and 98 deletions

View file

@ -51,6 +51,8 @@ ve.init.mw.ArticleTarget = function VeInitMwArticleTarget( config ) {
// A workaround, as default URI does not get updated after pushState (T74334)
this.currentUri = new mw.Uri( location.href );
this.section = null;
this.visibleSection = null;
this.visibleSectionOffset = null;
this.sectionTitle = null;
this.editSummaryValue = null;
this.initialEditSummary = null;
@ -1023,6 +1025,8 @@ ve.init.mw.ArticleTarget.prototype.clearState = function () {
this.originalHtml = null;
this.toolbarSaveButton = null;
this.section = null;
this.visibleSection = null;
this.visibleSectionOffset = null;
this.editNotices = [];
this.remoteNotices = [];
this.localNoticeMessages = [];
@ -1880,62 +1884,76 @@ ve.init.mw.ArticleTarget.prototype.getSaveDialogOpeningData = function () {
* Do nothing if this.section is undefined.
*/
ve.init.mw.ArticleTarget.prototype.restoreEditSection = function () {
var section = this.section,
surface = this.getSurface(),
mode = surface.getMode();
var section = this.section !== null ? this.section : this.visibleSection;
if ( section !== null && section !== 'new' && section !== '0' && section !== 'T-0' ) {
var headingText;
if ( mode === 'visual' ) {
var dmDoc = surface.getModel().getDocument();
// In mw.libs.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.
var headingModel;
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 ) {
var 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 );
}
if ( this.enableVisualSectionEditing && this.section !== null ) {
$( this.getElementWindow() ).scrollTop( 0 );
}
if ( section === null || section === 'new' || section === '0' || section === 'T-0' ) {
return;
}
var surface = this.getSurface(),
mode = surface.getMode(),
setExactScrollOffset = this.section === null && this.visibleSection !== null && this.visibleSectionOffset !== null,
// User clicked section edit link with visual section editing not available:
// Take them to the top of the section using goToHeading
goToStartOfHeading = this.section !== null && !this.enableVisualSectionEditing,
setEditSummary = this.section !== null;
var headingText;
if ( mode === 'visual' ) {
var dmDoc = surface.getModel().getDocument();
// In mw.libs.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.
var headingModel;
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 ) {
var headingView = surface.getView().getDocument().getDocumentNode().getNodeFromOffset( headingModel.getRange().start );
if ( setEditSummary && new mw.Uri().query.summary === undefined ) {
headingText = headingView.$element.text();
}
if ( setExactScrollOffset ) {
this.scrollToHeading( headingView, this.visibleSectionOffset );
} else if ( goToStartOfHeading ) {
this.goToHeading( headingView );
}
} else if ( mode === 'source' ) {
// With elements of extractSectionTitle + stripSectionName TODO:
// Arguably, we should just throw this through the API and then do
// the same extract-text pass we do in visual mode. Would save us
// having to think about wikitext here.
headingText = surface.getModel().getDocument().data.getText(
false,
surface.getModel().getDocument().getDocumentNode().children[ 0 ].getRange()
)
// Extract the title
.replace( /^\s*=+\s*(.*?)\s*=+\s*$/, '$1' )
// Remove links
.replace( /\[\[:?([^[|]+)\|([^[]+)\]\]/g, '$2' )
.replace( /\[\[:?([^[]+)\|?\]\]/g, '$1' )
.replace( new RegExp( '\\[(?:' + ve.init.platform.getUnanchoredExternalLinkUrlProtocolsRegExp().source + ')([^ ]+?) ([^\\[]+)\\]', 'ig' ), '$3' )
// Cheap HTML removal
.replace( /<[^>]+?>/g, '' );
}
if ( headingText ) {
this.initialEditSummary =
'/* ' +
ve.graphemeSafeSubstring( headingText, 0, 244 ) +
' */ ';
}
} else if ( mode === 'source' && setEditSummary ) {
// With elements of extractSectionTitle + stripSectionName TODO:
// Arguably, we should just throw this through the API and then do
// the same extract-text pass we do in visual mode. Would save us
// having to think about wikitext here.
headingText = surface.getModel().getDocument().data.getText(
false,
surface.getModel().getDocument().getDocumentNode().children[ 0 ].getRange()
)
// Extract the title
.replace( /^\s*=+\s*(.*?)\s*=+\s*$/, '$1' )
// Remove links
.replace( /\[\[:?([^[|]+)\|([^[]+)\]\]/g, '$2' )
.replace( /\[\[:?([^[]+)\|?\]\]/g, '$1' )
.replace( new RegExp( '\\[(?:' + ve.init.platform.getUnanchoredExternalLinkUrlProtocolsRegExp().source + ')([^ ]+?) ([^\\[]+)\\]', 'ig' ), '$3' )
// Cheap HTML removal
.replace( /<[^>]+?>/g, '' );
}
if ( headingText ) {
this.initialEditSummary =
'/* ' +
ve.graphemeSafeSubstring( headingText, 0, 244 ) +
' */ ';
}
};
@ -1988,11 +2006,13 @@ ve.init.mw.ArticleTarget.prototype.goToHeading = function ( headingNode ) {
* Scroll to a given heading in the document.
*
* @param {ve.ce.HeadingNode} headingNode Heading node to scroll to
* @param {number} [headingOffset=0] Set the top offset of the heading to a specific amount, relative
* to the surface viewport.
*/
ve.init.mw.ArticleTarget.prototype.scrollToHeading = function ( headingNode ) {
ve.init.mw.ArticleTarget.prototype.scrollToHeading = function ( headingNode, headingOffset ) {
var $window = $( this.getElementWindow() );
$window.scrollTop( headingNode.$element.offset().top - this.getSurface().padding.top );
$window.scrollTop( headingNode.$element.offset().top - ( this.getSurface().padding.top + ( headingOffset || 0 ) ) );
};
/**

View file

@ -238,6 +238,9 @@ ve.init.mw.DesktopArticleTarget.prototype.setupToolbar = function ( surface ) {
var toolbar = this.getToolbar();
// Allow the toolbar to start floating now if necessary
this.onContainerScroll();
ve.track( 'trace.setupToolbar.exit', { mode: mode } );
if ( !wasSetup ) {
// eslint-disable-next-line no-jquery/no-class-state
@ -249,14 +252,21 @@ ve.init.mw.DesktopArticleTarget.prototype.setupToolbar = function ( surface ) {
this.toolbarSetupDeferred.resolve();
} else {
setTimeout( function () {
var isFloating = toolbar.isFloating();
toolbar.$element
.css( 'height', toolbar.$bar[ 0 ].offsetHeight )
.addClass( 've-init-mw-desktopArticleTarget-toolbar-open' );
// For unfloated toolbar, transition the container hide to smoothly
// push the content down. Don't do this if the toolbar is floating to avoid movement.
if ( !isFloating ) {
toolbar.$element.css( 'height', toolbar.$bar[ 0 ].offsetHeight );
}
setTimeout( function () {
// Clear to allow growth during use and when resizing window
toolbar.$element
.css( 'height', '' )
.addClass( 've-init-mw-desktopArticleTarget-toolbar-opened' );
if ( !isFloating ) {
toolbar.$element.css( 'height', '' );
}
target.toolbarSetupDeferred.resolve();
}, 250 );
} );
@ -423,9 +433,6 @@ ve.init.mw.DesktopArticleTarget.prototype.activate = function ( dataPromise ) {
this.originalEditondbclick = mw.user.options.get( 'editondblclick' );
mw.user.options.set( 'editondblclick', 0 );
// Save the scroll position; will be restored by surfaceReady()
this.saveScrollPosition();
// User interface changes
this.changeDocumentTitle();
this.transformPage();
@ -730,8 +737,7 @@ ve.init.mw.DesktopArticleTarget.prototype.surfaceReady = function () {
var editNotices = this.getEditNotices(),
actionTools = this.actionsToolbar.tools,
surface = this.getSurface(),
target = this;
surface = this.getSurface();
this.activating = false;
@ -754,12 +760,6 @@ ve.init.mw.DesktopArticleTarget.prototype.surfaceReady = function () {
// existing page, or loading via an edit URL.
this.rebuildCategories( metaList.getItemsInGroup( 'mwCategory' ), true );
// Support: IE<=11
// IE requires us to defer before restoring the scroll position
setTimeout( function () {
target.restoreScrollPosition();
} );
// Parent method
ve.init.mw.DesktopArticleTarget.super.prototype.surfaceReady.apply( this, arguments );
@ -1070,28 +1070,6 @@ ve.init.mw.DesktopArticleTarget.prototype.getSaveDialogOpeningData = function ()
return data;
};
/**
* Remember the window's scroll position.
*/
ve.init.mw.DesktopArticleTarget.prototype.saveScrollPosition = function () {
if ( ( this.getDefaultMode() === 'source' || this.enableVisualSectionEditing ) && this.section !== null ) {
// Reset scroll to top if doing real section editing
this.scrollTop = 0;
} else {
this.scrollTop = $( window ).scrollTop();
}
};
/**
* Restore the window's scroll position.
*/
ve.init.mw.DesktopArticleTarget.prototype.restoreScrollPosition = function () {
if ( this.scrollTop !== null ) {
$( window ).scrollTop( this.scrollTop );
this.scrollTop = null;
}
};
/**
* @inheritdoc
*/

View file

@ -20,13 +20,13 @@
*/
/* Only hide the #toc inside the original article, not generated ones in VE (T187636) */
.ve-activated .ve-init-mw-desktopArticleTarget-editableContent #toc,
.ve-activated #siteNotice,
.ve-activated .mw-indicators,
.ve-activated #t-print,
.ve-activated #t-permalink,
.ve-activated #p-coll-print_export,
.ve-activated #t-cite,
.ve-active .ve-init-mw-desktopArticleTarget-editableContent #toc,
.ve-active #siteNotice,
.ve-active .mw-indicators,
.ve-active #t-print,
.ve-active #t-permalink,
.ve-active #p-coll-print_export,
.ve-active #t-cite,
.ve-deactivating .ve-ui-surface,
.ve-active .ve-init-mw-desktopArticleTarget-editableContent,
.ve-active .ve-init-mw-tempWikitextEditorWidget {

View file

@ -346,7 +346,9 @@
*
* @private
* @param {string} mode Target mode: 'visual' or 'source'
* @param {string} [section] Section to edit (currently just source mode)
* @param {string} [section] Section to edit.
* If visual section editing is not enabled, we will jump to the start of this section, and still
* the heading to prefix the edit summary.
* @param {jQuery.Promise} [tPromise] 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)
*/
@ -393,6 +395,29 @@
.then( incrementLoadingProgress );
}
var visibleSection = null;
var visibleSectionOffset = null;
if ( section === null ) {
var firstVisibleEditSection = null;
$( '#mw-content-text .mw-editsection' ).each( function () {
var top = this.getBoundingClientRect().top;
if ( top > 0 ) {
firstVisibleEditSection = this;
// break
return false;
}
} );
if ( firstVisibleEditSection ) {
var firstVisibleSectionLink = firstVisibleEditSection.querySelector( 'a' );
var linkUri = new mw.Uri( firstVisibleSectionLink.href );
visibleSection = parseSection( linkUri.query.section );
var firstVisibleHeading = $( firstVisibleEditSection ).closest( 'h1, h2, h3, h4, h5, h6' )[ 0 ];
visibleSectionOffset = firstVisibleHeading.getBoundingClientRect().top;
}
}
showLoading( mode );
incrementLoadingProgress();
active = true;
@ -400,6 +425,9 @@
tPromise = tPromise || getTarget( mode, section );
tPromise
.then( function ( target ) {
target.visibleSection = visibleSection;
target.visibleSectionOffset = visibleSectionOffset;
incrementLoadingProgress();
target.on( 'deactivate', function () {
active = false;