Merge "[ToC] Show new/modified sections after publishing an edit (new floating ToC)"

This commit is contained in:
jenkins-bot 2022-08-16 08:37:30 +00:00 committed by Gerrit Code Review
commit 8c3a4792e9
11 changed files with 319 additions and 27390 deletions

View File

@ -75,6 +75,9 @@ class SkinVector22 extends SkinVector {
/**
* Annotates table of contents data with Vector-specific information.
*
* In tableOfContents.js we have tableOfContents::getTableOfContentsSectionsData(),
* that yields the same result as this function, please make sure to keep them in sync.
*
* @param array $tocData
* @return array
*/

View File

@ -6,7 +6,7 @@
<button class="vector-toc-collapse-button">{{msg-vector-toc-toggle-position-title}}</button>
</p>
</div>
<ul class="sidebar-toc-contents">
<ul class="sidebar-toc-contents" id="mw-panel-toc-list">
{{#is-vector-toc-beginning-enabled}}
<li id="toc-mw-content-text"
class="sidebar-toc-list-item sidebar-toc-level-1">

View File

@ -3,6 +3,7 @@
module.exports = {
moduleNameMapper: {
'^./templates/(.*).mustache': '<rootDir>/includes/templates/$1.mustache'
},
// Automatically clear mock calls and instances between every test
@ -51,6 +52,7 @@ module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.mustache?$': 'mustache-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest'
}
};

27471
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,7 @@
"less": "3.8.1",
"less-loader": "4.1.0",
"mustache": "3.0.1",
"mustache-jest": "1.1.1",
"node-fetch": "2.6.7",
"pa11y": "6.1.1",
"postcss-less": "6.0.0",

View File

@ -7,6 +7,16 @@ const PARENT_SECTION_CLASS = 'sidebar-toc-level-1';
const LINK_CLASS = 'sidebar-toc-link';
const TOGGLE_CLASS = 'sidebar-toc-toggle';
const TOC_COLLAPSED_CLASS = 'vector-toc-collapsed';
const TOC_ID = 'mw-panel-toc';
/**
* TableOfContents Mustache templates
*/
const templateBody = require( /** @type {string} */ ( './templates/TableOfContents.mustache' ) );
const templateTocLine = require( /** @type {string} */ ( './templates/TableOfContents__line.mustache' ) );
/**
* TableOfContents Config object for filling mustache templates
*/
const tableOfContentsConfig = require( /** @type {string} */ ( './tableOfContentsConfig.json' ) );
/**
* @callback onHeadingClick
@ -30,6 +40,39 @@ const TOC_COLLAPSED_CLASS = 'vector-toc-collapsed';
* @property {onToggleCollapse} onToggleCollapse Called when collapse toggle buttons are clicked.
*/
/**
* @typedef {Object} Section
* @property {number} toclevel
* @property {string} anchor
* @property {string} line
* @property {string} number
* @property {string} index
* @property {number} byteoffset
* @property {string} fromtitle
* @property {boolean} is-parent-section
* @property {boolean} is-top-level-section
* @property {Section[]} array-sections
* @property {string} level
*/
/**
* @typedef {Object} SectionsListData
* @property {boolean} is-vector-toc-beginning-enabled
* @property {Section[]} array-sections
* @property {boolean} vector-is-collapse-sections-enabled
* @property {string} msg-vector-toc-heading
* @property {number} number-section-count
* @property {string} msg-vector-toc-beginning
* @property {string} msg-vector-toc-toggle-position-title
* @property {string} msg-vector-toc-toggle-position-sidebar
*/
/**
* @typedef {Object} ArraySectionsData
* @property {number} number-section-count
* @property {Section[]} array-sections
*/
/**
* Initializes the sidebar's Table of Contents.
*
@ -344,15 +387,150 @@ module.exports = function tableOfContents( props ) {
bindSubsectionToggleListeners();
bindCollapseToggleListeners();
// Hide TOC button on VE activation
mw.hook( 've.activationStart' ).add( () => {
const tocButton = document.getElementById( 'vector-toc-collapsed-button' );
if ( tocButton ) {
tocButton.style.display = 'none';
}
mw.hook( 'wikipage.tableOfContents' ).add( reloadTableOfContents );
}
/**
* Reexpands all sections that were expanded before the table of contents was reloaded.
* Edited Sections are not reexpanded, as the ID of the edited section is changed after reload.
*/
function reExpandSections() {
initializeExpandedStatus();
const expandedSectionIds = getExpandedSectionIds();
for ( const id of expandedSectionIds ) {
expandSection( id );
}
}
/**
* Reloads the table of contents from saved data
*
* @param {Section[]} sections
*/
function reloadTableOfContents( sections ) {
mw.loader.using( 'mediawiki.template.mustache' ).then( () => {
reloadPartialHTML( getTableOfContentsHTML( sections ), TOC_ID );
// Rebind event listeners.
bindSubsectionToggleListeners();
// Reexpand sections that were expanded before the table of contents was reloaded.
reExpandSections();
// Initialize Collapse toggle buttons
bindCollapseToggleListeners();
} );
}
/**
* Replaces the contents of the given element with the given HTML
*
* @param {string} html
* @param {string} elementId
* @param {boolean} setInnerHTML
*/
function reloadPartialHTML( html, elementId, setInnerHTML = true ) {
const htmlElement = document.getElementById( elementId );
if ( htmlElement ) {
if ( setInnerHTML ) {
htmlElement.innerHTML = html;
} else if ( htmlElement.outerHTML ) {
htmlElement.outerHTML = html;
} else { // IF outerHTML property access is not supported
const tmpContainer = document.createElement( 'div' );
tmpContainer.innerHTML = html.trim();
const childNode = tmpContainer.firstChild;
if ( childNode ) {
const tmpElement = document.createElement( 'div' );
tmpElement.setAttribute( 'id', `div-tmp-${elementId}` );
const parentNode = htmlElement.parentNode;
if ( parentNode ) {
parentNode.replaceChild( tmpElement, htmlElement );
parentNode.replaceChild( childNode, tmpElement );
}
}
}
}
}
/**
* Generates the HTML for the table of contents.
*
* @param {Section[]} sections
* @return {string}
*/
function getTableOfContentsHTML( sections ) {
return getTableOfContentsListHtml( getTableOfContentsData( sections ) );
}
/**
* Generates the table of contents List HTML from the templates
*
* @param {Object} data
* @return {string}
*/
function getTableOfContentsListHtml( data ) {
// @ts-ignore
const mustacheCompiler = mw.template.getCompiler( 'mustache' );
const compiledTemplateBody = mustacheCompiler.compile( templateBody );
const compiledTemplateTocLine = mustacheCompiler.compile( templateTocLine );
// Identifier 'TableOfContents__line' is not in camel case
// (template name is 'TableOfContents__line')
const partials = {
TableOfContents__line: compiledTemplateTocLine // eslint-disable-line camelcase
};
return compiledTemplateBody.render( data, partials ).html();
}
/**
* @param {Section[]} sections
* @return {SectionsListData}
*/
function getTableOfContentsData( sections ) {
return {
'number-section-count': sections.length,
'msg-vector-toc-heading': mw.message( 'vector-toc-heading' ).text(),
'msg-vector-toc-toggle-position-sidebar': mw.message( 'vector-toc-toggle-position-sidebar' ).text(),
'msg-vector-toc-toggle-position-title': mw.message( 'vector-toc-toggle-position-title' ).text(),
'msg-vector-toc-beginning': mw.message( 'vector-toc-beginning' ).text(),
'array-sections': getTableOfContentsSectionsData( sections, 1 ),
'vector-is-collapse-sections-enabled': sections.length >= tableOfContentsConfig.VectorTableOfContentsCollapseAtCount,
'is-vector-toc-beginning-enabled': tableOfContentsConfig.VectorTableOfContentsBeginning
};
}
/**
* Prepares the data for rendering the table of contents,
* nesting child sections within their parent sections.
* This shoul yield the same result as the php function SkinVector22::getTocData(),
* please make sure to keep them in sync.
*
* @param {Section[]} sections
* @param {number} toclevel
* @return {Section[]}
*/
function getTableOfContentsSectionsData( sections, toclevel = 1 ) {
const data = [];
for ( let i = 0; i < sections.length; i++ ) {
const section = sections[ i ];
if ( section.toclevel === toclevel ) {
const childSections = getTableOfContentsSectionsData(
sections.slice( i + 1 ),
toclevel + 1
);
section[ 'array-sections' ] = childSections;
section[ 'is-top-level-section' ] = toclevel === 1;
section[ 'is-parent-section' ] = Object.keys( childSections ).length > 0;
data.push( section );
}
// Child section belongs to a higher parent.
if ( section.toclevel < toclevel ) {
return data;
}
}
return data;
}
initialize();
/**

View File

@ -0,0 +1,5 @@
{
"@doc": "This is a virtual JSON generated by ResourceLoader. This file is used by Jest.",
"VectorTableOfContentsCollapseAtCount": 20,
"VectorTableOfContentsBeginning": true
}

View File

@ -26,6 +26,12 @@
z-index: @z-index-menu;
}
.ve-active {
#vector-toc-collapsed-button {
display: none !important; /* stylelint-disable-line declaration-no-important */
}
}
// Override button styles for the "move to sidebar/hide" links. Default hide.
.vector-toc-collapse-button,
.vector-toc-uncollapse-button {

View File

@ -331,6 +331,23 @@
{
"name": "resources/skins.vector.es6/config.json",
"callback": "MediaWiki\\Skins\\Vector\\Hooks::getVectorResourceLoaderConfig"
},
{
"name": "resources/skins.vector.es6/tableOfContentsConfig.json",
"config": [
"VectorTableOfContentsCollapseAtCount",
"VectorTableOfContentsBeginning"
]
},
{
"name": "resources/skins.vector.es6/templates/TableOfContents.mustache",
"file": "includes/templates/TableOfContents.mustache",
"type": "text"
},
{
"name": "resources/skins.vector.es6/templates/TableOfContents__line.mustache",
"file": "includes/templates/TableOfContents__line.mustache",
"type": "text"
}
],
"dependencies": [
@ -340,6 +357,12 @@
"mediawiki.page.watch.ajax",
"mediawiki.util",
"mediawiki.experiments"
],
"messages": [
"vector-toc-beginning",
"vector-toc-heading",
"vector-toc-toggle-position-sidebar",
"vector-toc-toggle-position-title"
]
},
"skins.vector.js": {

View File

@ -9,7 +9,7 @@ exports[`Table of contents renders when \`vector-is-collapse-sections-enabled\`
<button class=\\"vector-toc-collapse-button\\"></button>
</p>
</div>
<ul class=\\"sidebar-toc-contents\\">
<ul class=\\"sidebar-toc-contents\\" id=\\"mw-panel-toc-list\\">
<li id=\\"toc-mw-content-text\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<a href=\\"#top-page\\" class=\\"sidebar-toc-link\\">
<div class=\\"sidebar-toc-text\\">Beginning</div>
@ -72,7 +72,7 @@ exports[`Table of contents renders when \`vector-is-collapse-sections-enabled\`
<button class=\\"vector-toc-collapse-button\\"></button>
</p>
</div>
<ul class=\\"sidebar-toc-contents\\">
<ul class=\\"sidebar-toc-contents\\" id=\\"mw-panel-toc-list\\">
<li id=\\"toc-mw-content-text\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<a href=\\"#top-page\\" class=\\"sidebar-toc-link\\">
<div class=\\"sidebar-toc-text\\">Beginning</div>

View File

@ -199,7 +199,7 @@ describe( 'Table of contents', () => {
const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${toc.TOGGLE_CLASS}` ) );
expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );
expect( mw.hook ).toBeCalledWith( 've.activationStart' );
expect( mw.hook ).toBeCalledWith( 'wikipage.tableOfContents' );
} );
test( 'when expanding sections', () => {