2021-10-20 18:58:49 +00:00
|
|
|
// Enable Vector features limited to ES6 browse
|
2021-10-26 23:37:56 +00:00
|
|
|
const
|
|
|
|
stickyHeader = require( './stickyHeader.js' ),
|
|
|
|
scrollObserver = require( './scrollObserver.js' ),
|
2022-03-17 23:01:17 +00:00
|
|
|
initExperiment = require( './AB.js' ),
|
2022-01-21 20:15:34 +00:00
|
|
|
initSectionObserver = require( './sectionObserver.js' ),
|
|
|
|
initTableOfContents = require( './tableOfContents.js' ),
|
2022-11-28 21:08:40 +00:00
|
|
|
pinnableElement = require( './pinnableElement.js' ),
|
2023-03-23 20:03:13 +00:00
|
|
|
popupNotification = require( './popupNotification.js' ),
|
2023-02-23 21:23:46 +00:00
|
|
|
features = require( './features.js' ),
|
2022-02-08 21:14:33 +00:00
|
|
|
deferUntilFrame = require( './deferUntilFrame.js' ),
|
2023-04-10 18:56:19 +00:00
|
|
|
ABTestConfig = require( /** @type {string} */ ( './activeABTest.json' ) ),
|
2022-07-01 20:19:57 +00:00
|
|
|
STICKY_HEADER_VISIBLE_CLASS = 'vector-sticky-header-visible',
|
2022-12-20 22:58:44 +00:00
|
|
|
TOC_ID = 'vector-toc',
|
2022-01-21 20:15:34 +00:00
|
|
|
BODY_CONTENT_ID = 'bodyContent',
|
|
|
|
HEADLINE_SELECTOR = '.mw-headline',
|
2022-03-17 23:01:17 +00:00
|
|
|
TOC_SECTION_ID_PREFIX = 'toc-',
|
2022-12-09 21:27:12 +00:00
|
|
|
PAGE_TITLE_INTERSECTION_CLASS = 'vector-below-page-title';
|
2021-10-20 18:58:49 +00:00
|
|
|
|
2022-07-01 20:19:57 +00:00
|
|
|
const belowDesktopMedia = window.matchMedia( '(max-width: 999px)' );
|
|
|
|
|
2022-03-17 16:54:26 +00:00
|
|
|
/**
|
|
|
|
* @callback OnIntersection
|
|
|
|
* @param {HTMLElement} element The section that triggered the new intersection change.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @ignore
|
|
|
|
* @param {Function} changeActiveSection
|
|
|
|
* @return {OnIntersection}
|
|
|
|
*/
|
|
|
|
const getHeadingIntersectionHandler = ( changeActiveSection ) => {
|
|
|
|
/**
|
|
|
|
* @param {HTMLElement} section
|
|
|
|
*/
|
|
|
|
return ( section ) => {
|
|
|
|
const headline = section.classList.contains( 'mw-body-content' ) ?
|
|
|
|
section :
|
|
|
|
section.querySelector( HEADLINE_SELECTOR );
|
|
|
|
if ( headline ) {
|
|
|
|
changeActiveSection( `${TOC_SECTION_ID_PREFIX}${headline.id}` );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-04-14 03:58:05 +00:00
|
|
|
/**
|
|
|
|
* Initialize sticky header AB tests and determine whether to show the sticky header
|
|
|
|
* based on which buckets the user is in.
|
|
|
|
*
|
|
|
|
* @typedef {Object} InitStickyHeaderABTests
|
|
|
|
* @property {boolean} disableEditIcons - Should the sticky header have an edit icon
|
|
|
|
* @property {boolean} showStickyHeader - Should the sticky header be shown
|
|
|
|
* @param {ABTestConfig} abConfig
|
|
|
|
* @param {boolean} isStickyHeaderFeatureAllowed and the user is logged in
|
|
|
|
* @param {function(ABTestConfig): initExperiment.WebABTest} getEnabledExperiment
|
|
|
|
* @return {InitStickyHeaderABTests}
|
|
|
|
*/
|
|
|
|
function initStickyHeaderABTests( abConfig, isStickyHeaderFeatureAllowed, getEnabledExperiment ) {
|
2023-01-19 20:39:55 +00:00
|
|
|
let showStickyHeader = isStickyHeaderFeatureAllowed,
|
2022-04-14 03:58:05 +00:00
|
|
|
stickyHeaderExperiment,
|
2023-01-19 20:39:55 +00:00
|
|
|
disableEditIcons = true;
|
2022-04-14 03:58:05 +00:00
|
|
|
|
2022-06-15 22:01:02 +00:00
|
|
|
// One of the sticky header AB tests is specified in the config
|
|
|
|
const abTestName = abConfig.name,
|
|
|
|
isStickyHeaderExperiment = abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ||
|
|
|
|
abTestName === stickyHeader.STICKY_HEADER_EDIT_EXPERIMENT_NAME;
|
|
|
|
|
2022-04-14 03:58:05 +00:00
|
|
|
// Determine if user is eligible for sticky header AB test
|
|
|
|
if (
|
|
|
|
isStickyHeaderFeatureAllowed && // The sticky header can be shown on the page
|
|
|
|
abConfig.enabled && // An AB test config is enabled
|
2022-06-15 22:01:02 +00:00
|
|
|
isStickyHeaderExperiment // The AB test is one of the sticky header experiments
|
2022-04-14 03:58:05 +00:00
|
|
|
) {
|
|
|
|
// If eligible, initialize the AB test
|
|
|
|
stickyHeaderExperiment = getEnabledExperiment( abConfig );
|
2023-01-19 20:39:55 +00:00
|
|
|
disableEditIcons = true;
|
2022-04-14 03:58:05 +00:00
|
|
|
|
Sticky header AB test bucketing for 2 treatment buckets
For idwiki/viwiki, we wish to run the sticky header edit button AB
test so that treatment1 group sees the sticky header without edit
buttons, treatment2 groups sees the sticky header with edit buttons,
and the control/unsampled groups see no sticky header at all.
This patch overrides the configuration to make the sticky header
w/o edit buttons for treatment1, sticky header w/ edit buttons for
treatment2, and hides sticky header for everyone else. This depends
on a configuration with the treatment groups having "treatment1"
and "treatment2" as substrings in their bucket names.
The full configuration for idwiki/viwiki would be something like
the following:
```
$wgVectorStickyHeader = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorStickyHeaderEdit = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorWebABTestEnrollment = [
"name" => "vector.sticky_header_edit",
"enabled" => true,
"buckets" => [
"unsampled" => [
"samplingRate" => 0
],
"noStickyHeaderControl" => [
"samplingRate" => 0.34
],
"stickyHeaderNoEditButtonTreatment1" => [
"samplingRate" => 0.33
],
"stickyHeaderEditButtonTreatment2" => [
"samplingRate" => 0.33
]
],
];
```
Bug: T312573
Change-Id: I15c360fdf5393f5594602acc33b5b916e904016d
2022-07-07 19:06:14 +00:00
|
|
|
// If running initial or edit AB test, show sticky header to treatment groups
|
|
|
|
// only. Unsampled and control buckets do not see sticky header.
|
|
|
|
if ( abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ||
|
|
|
|
abTestName === stickyHeader.STICKY_HEADER_EDIT_EXPERIMENT_NAME
|
|
|
|
) {
|
2023-01-19 20:39:55 +00:00
|
|
|
showStickyHeader = stickyHeaderExperiment.isInTreatmentBucket();
|
2022-04-14 03:58:05 +00:00
|
|
|
}
|
|
|
|
|
Sticky header AB test bucketing for 2 treatment buckets
For idwiki/viwiki, we wish to run the sticky header edit button AB
test so that treatment1 group sees the sticky header without edit
buttons, treatment2 groups sees the sticky header with edit buttons,
and the control/unsampled groups see no sticky header at all.
This patch overrides the configuration to make the sticky header
w/o edit buttons for treatment1, sticky header w/ edit buttons for
treatment2, and hides sticky header for everyone else. This depends
on a configuration with the treatment groups having "treatment1"
and "treatment2" as substrings in their bucket names.
The full configuration for idwiki/viwiki would be something like
the following:
```
$wgVectorStickyHeader = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorStickyHeaderEdit = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorWebABTestEnrollment = [
"name" => "vector.sticky_header_edit",
"enabled" => true,
"buckets" => [
"unsampled" => [
"samplingRate" => 0
],
"noStickyHeaderControl" => [
"samplingRate" => 0.34
],
"stickyHeaderNoEditButtonTreatment1" => [
"samplingRate" => 0.33
],
"stickyHeaderEditButtonTreatment2" => [
"samplingRate" => 0.33
]
],
];
```
Bug: T312573
Change-Id: I15c360fdf5393f5594602acc33b5b916e904016d
2022-07-07 19:06:14 +00:00
|
|
|
// If running edit-button AB test, the edit buttons in sticky header are shown
|
|
|
|
// to second treatment group only.
|
2022-06-15 22:01:02 +00:00
|
|
|
if ( abTestName === stickyHeader.STICKY_HEADER_EDIT_EXPERIMENT_NAME ) {
|
Sticky header AB test bucketing for 2 treatment buckets
For idwiki/viwiki, we wish to run the sticky header edit button AB
test so that treatment1 group sees the sticky header without edit
buttons, treatment2 groups sees the sticky header with edit buttons,
and the control/unsampled groups see no sticky header at all.
This patch overrides the configuration to make the sticky header
w/o edit buttons for treatment1, sticky header w/ edit buttons for
treatment2, and hides sticky header for everyone else. This depends
on a configuration with the treatment groups having "treatment1"
and "treatment2" as substrings in their bucket names.
The full configuration for idwiki/viwiki would be something like
the following:
```
$wgVectorStickyHeader = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorStickyHeaderEdit = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorWebABTestEnrollment = [
"name" => "vector.sticky_header_edit",
"enabled" => true,
"buckets" => [
"unsampled" => [
"samplingRate" => 0
],
"noStickyHeaderControl" => [
"samplingRate" => 0.34
],
"stickyHeaderNoEditButtonTreatment1" => [
"samplingRate" => 0.33
],
"stickyHeaderEditButtonTreatment2" => [
"samplingRate" => 0.33
]
],
];
```
Bug: T312573
Change-Id: I15c360fdf5393f5594602acc33b5b916e904016d
2022-07-07 19:06:14 +00:00
|
|
|
if ( stickyHeaderExperiment.isInTreatmentBucket( '1' ) ) {
|
2023-01-19 20:39:55 +00:00
|
|
|
disableEditIcons = true;
|
Sticky header AB test bucketing for 2 treatment buckets
For idwiki/viwiki, we wish to run the sticky header edit button AB
test so that treatment1 group sees the sticky header without edit
buttons, treatment2 groups sees the sticky header with edit buttons,
and the control/unsampled groups see no sticky header at all.
This patch overrides the configuration to make the sticky header
w/o edit buttons for treatment1, sticky header w/ edit buttons for
treatment2, and hides sticky header for everyone else. This depends
on a configuration with the treatment groups having "treatment1"
and "treatment2" as substrings in their bucket names.
The full configuration for idwiki/viwiki would be something like
the following:
```
$wgVectorStickyHeader = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorStickyHeaderEdit = [
"logged_in" => true,
"logged_out" => false,
];
$wgVectorWebABTestEnrollment = [
"name" => "vector.sticky_header_edit",
"enabled" => true,
"buckets" => [
"unsampled" => [
"samplingRate" => 0
],
"noStickyHeaderControl" => [
"samplingRate" => 0.34
],
"stickyHeaderNoEditButtonTreatment1" => [
"samplingRate" => 0.33
],
"stickyHeaderEditButtonTreatment2" => [
"samplingRate" => 0.33
]
],
];
```
Bug: T312573
Change-Id: I15c360fdf5393f5594602acc33b5b916e904016d
2022-07-07 19:06:14 +00:00
|
|
|
}
|
|
|
|
if ( stickyHeaderExperiment.isInTreatmentBucket( '2' ) ) {
|
2023-01-19 20:39:55 +00:00
|
|
|
disableEditIcons = false;
|
2022-04-14 03:58:05 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-19 20:39:55 +00:00
|
|
|
}
|
|
|
|
if ( !abConfig.enabled ) {
|
|
|
|
disableEditIcons = false;
|
2022-04-14 03:58:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2023-01-19 20:39:55 +00:00
|
|
|
showStickyHeader,
|
|
|
|
disableEditIcons
|
2022-04-14 03:58:05 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-07-01 20:19:57 +00:00
|
|
|
/*
|
|
|
|
* Updates TOC's location in the DOM (in sidebar or sticky header)
|
|
|
|
* depending on if the TOC is collapsed and if the sticky header is visible
|
|
|
|
*
|
|
|
|
* @return {void}
|
|
|
|
*/
|
|
|
|
const updateTocLocation = () => {
|
2023-02-23 21:23:46 +00:00
|
|
|
const isPinned = features.isEnabled( 'toc-pinned' );
|
2022-07-01 20:19:57 +00:00
|
|
|
const isStickyHeaderVisible = document.body.classList.contains( STICKY_HEADER_VISIBLE_CLASS );
|
|
|
|
const isBelowDesktop = belowDesktopMedia.matches;
|
2022-10-20 21:32:07 +00:00
|
|
|
|
2022-11-21 22:16:07 +00:00
|
|
|
const pinnedContainerId = 'vector-toc-pinned-container';
|
2023-01-13 20:56:22 +00:00
|
|
|
const stickyHeaderUnpinnedContainerId = 'vector-sticky-header-toc-unpinned-container';
|
2022-11-21 22:16:07 +00:00
|
|
|
const pageTitlebarUnpinnedContainerId = 'vector-page-titlebar-toc-unpinned-container';
|
|
|
|
|
|
|
|
let newContainerId = '';
|
|
|
|
if ( isPinned ) {
|
2022-11-28 20:38:57 +00:00
|
|
|
if ( isBelowDesktop ) {
|
2022-11-21 22:16:07 +00:00
|
|
|
// Automatically move the ToC into the page titlebar when pinned on smaller resolutions
|
|
|
|
newContainerId = pageTitlebarUnpinnedContainerId;
|
|
|
|
} else {
|
|
|
|
newContainerId = pinnedContainerId;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if ( isStickyHeaderVisible && !isBelowDesktop ) {
|
|
|
|
newContainerId = stickyHeaderUnpinnedContainerId;
|
|
|
|
} else {
|
|
|
|
newContainerId = pageTitlebarUnpinnedContainerId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-13 20:56:22 +00:00
|
|
|
pinnableElement.movePinnableElement( TOC_ID, newContainerId );
|
2022-07-01 20:19:57 +00:00
|
|
|
};
|
|
|
|
|
2023-02-16 01:20:00 +00:00
|
|
|
/**
|
2023-03-06 18:51:28 +00:00
|
|
|
* Return the computed value of the `scroll-margin-top` CSS property of the document element
|
|
|
|
* which is also used for the scroll intersection threshold (T317661).
|
2023-03-01 17:23:43 +00:00
|
|
|
*
|
2023-03-06 18:51:28 +00:00
|
|
|
* @return {number} Value of scroll-margin-top OR 75 if falsy.
|
|
|
|
* 75 derived from @scroll-padding-top LESS variable
|
|
|
|
* https://gerrit.wikimedia.org/r/c/mediawiki/skins/Vector/+/894696/3/resources/common/variables.less ?
|
2023-02-28 19:48:31 +00:00
|
|
|
*/
|
2023-03-06 18:51:28 +00:00
|
|
|
function getDocumentScrollPaddingTop() {
|
|
|
|
const defaultScrollPaddingTop = 75;
|
2023-03-01 17:23:43 +00:00
|
|
|
const documentStyles = getComputedStyle( document.documentElement );
|
|
|
|
const scrollPaddingTopString = documentStyles.getPropertyValue( 'scroll-padding-top' );
|
2023-03-06 18:51:28 +00:00
|
|
|
return ( parseInt( scrollPaddingTopString, 10 ) || defaultScrollPaddingTop );
|
2023-02-28 19:48:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-02-16 01:20:00 +00:00
|
|
|
* @param {HTMLElement|null} tocElement
|
|
|
|
* @param {HTMLElement|null} bodyContent
|
|
|
|
* @param {initSectionObserver} initSectionObserverFn
|
|
|
|
* @return {tableOfContents|null}
|
|
|
|
*/
|
2023-02-28 19:48:31 +00:00
|
|
|
const setupTableOfContents = ( tocElement, bodyContent, initSectionObserverFn ) => {
|
2023-02-18 00:31:56 +00:00
|
|
|
if ( !(
|
|
|
|
tocElement &&
|
2023-02-16 01:20:00 +00:00
|
|
|
bodyContent
|
2023-02-18 00:31:56 +00:00
|
|
|
) ) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-03-03 08:32:02 +00:00
|
|
|
const handleTocSectionChange = () => {
|
|
|
|
// eslint-disable-next-line no-use-before-define
|
|
|
|
sectionObserver.pause();
|
2023-02-18 00:31:56 +00:00
|
|
|
|
2023-03-03 08:32:02 +00:00
|
|
|
// T297614: We want the link that the user has clicked inside the TOC or the
|
|
|
|
// section that corresponds to the hashchange event to be "active" (e.g.
|
|
|
|
// bolded) regardless of whether the browser's scroll position corresponds
|
|
|
|
// to that section. Therefore, we need to temporarily ignore section
|
|
|
|
// observer until the browser has finished scrolling to the section (if
|
|
|
|
// needed).
|
|
|
|
//
|
|
|
|
// However, because the scroll event happens asynchronously after the user
|
|
|
|
// clicks on a link and may not even happen at all (e.g. the user has
|
|
|
|
// scrolled all the way to the bottom and clicks a section that is already
|
|
|
|
// in the viewport), determining when we should resume section observer is a
|
|
|
|
// bit tricky.
|
|
|
|
//
|
|
|
|
// Because a scroll event may not even be triggered after clicking the link,
|
|
|
|
// we instead allow the browser to perform a maximum number of repaints
|
|
|
|
// before resuming sectionObserver. Per T297614#7687656, Firefox 97.0 wasn't
|
|
|
|
// consistently activating the table of contents section that the user
|
|
|
|
// clicked even after waiting 2 frames. After further investigation, it
|
|
|
|
// sometimes waits up to 3 frames before painting the new scroll position so
|
|
|
|
// we have that as the limit.
|
|
|
|
deferUntilFrame( () => {
|
2023-02-18 00:31:56 +00:00
|
|
|
// eslint-disable-next-line no-use-before-define
|
2023-03-03 08:32:02 +00:00
|
|
|
sectionObserver.resume();
|
|
|
|
}, 3 );
|
|
|
|
};
|
|
|
|
|
|
|
|
const tableOfContents = initTableOfContents( {
|
|
|
|
container: tocElement,
|
|
|
|
onHeadingClick: handleTocSectionChange,
|
|
|
|
onHashChange: handleTocSectionChange,
|
2023-02-18 00:31:56 +00:00
|
|
|
onTogglePinned: () => {
|
|
|
|
updateTocLocation();
|
|
|
|
pinnableElement.setFocusAfterToggle( TOC_ID );
|
2023-03-23 20:03:13 +00:00
|
|
|
if ( !features.isEnabled( 'toc-pinned' ) ) {
|
|
|
|
const isStickyHeaderVisible = document.body.classList
|
|
|
|
.contains( STICKY_HEADER_VISIBLE_CLASS );
|
|
|
|
const containerSelector = !isStickyHeaderVisible ?
|
|
|
|
'.vector-page-titlebar .vector-toc-landmark' : '#vector-sticky-header .vector-toc-landmark';
|
2023-04-03 21:16:52 +00:00
|
|
|
const container = /** @type {HTMLElement} */(
|
|
|
|
document.querySelector( containerSelector )
|
|
|
|
);
|
2023-03-23 20:03:13 +00:00
|
|
|
if ( container ) {
|
2023-05-01 10:11:29 +00:00
|
|
|
const containerId = !isStickyHeaderVisible ? 'toc-page-titlebar' : 'toc-sticky-header';
|
|
|
|
popupNotification.add( container, mw.message( 'vector-toc-unpinned-popup' ).text(), containerId )
|
2023-04-24 12:26:48 +00:00
|
|
|
.then( ( popupWidget ) => {
|
|
|
|
if ( popupWidget ) {
|
|
|
|
popupNotification.show( popupWidget );
|
|
|
|
}
|
|
|
|
} );
|
2023-03-23 20:03:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-18 00:31:56 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
const headingSelector = [
|
|
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
|
|
|
|
].map( ( tag ) => `.mw-parser-output ${tag}` ).join( ',' );
|
2023-02-15 23:29:56 +00:00
|
|
|
const elements = () => bodyContent.querySelectorAll( `${headingSelector}, .mw-body-content` );
|
|
|
|
|
2023-02-16 01:20:00 +00:00
|
|
|
const sectionObserver = initSectionObserverFn( {
|
2023-02-15 23:29:56 +00:00
|
|
|
elements: elements(),
|
2023-03-06 18:51:28 +00:00
|
|
|
topMargin: getDocumentScrollPaddingTop(),
|
2023-02-18 00:31:56 +00:00
|
|
|
onIntersection: getHeadingIntersectionHandler( tableOfContents.changeActiveSection )
|
|
|
|
} );
|
2023-02-15 23:29:56 +00:00
|
|
|
const updateElements = () => {
|
|
|
|
sectionObserver.resume();
|
|
|
|
sectionObserver.setElements( elements() );
|
|
|
|
};
|
|
|
|
mw.hook( 've.activationStart' ).add( () => {
|
|
|
|
sectionObserver.pause();
|
|
|
|
} );
|
2023-02-16 01:20:00 +00:00
|
|
|
mw.hook( 'wikipage.tableOfContents' ).add( function ( sections ) {
|
|
|
|
tableOfContents.reloadTableOfContents( sections ).then( function () {
|
|
|
|
mw.hook( 'wikipage.tableOfContents.vector' ).fire( sections );
|
|
|
|
updateElements();
|
|
|
|
} );
|
|
|
|
} );
|
2023-02-15 23:29:56 +00:00
|
|
|
mw.hook( 've.deactivationComplete' ).add( updateElements );
|
2023-03-03 08:32:02 +00:00
|
|
|
|
|
|
|
const setInitialActiveSection = () => {
|
|
|
|
const hash = location.hash.slice( 1 );
|
|
|
|
// If hash fragment is blank, determine the active section with section
|
|
|
|
// observer.
|
|
|
|
if ( hash === '' ) {
|
|
|
|
sectionObserver.calcIntersection();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// T325086: If hash fragment is present and corresponds to a toc section,
|
|
|
|
// expand the section.
|
|
|
|
const hashSection = /** @type {HTMLElement|null} */ ( mw.util.getTargetFromFragment( `${TOC_SECTION_ID_PREFIX}${hash}` ) );
|
|
|
|
if ( hashSection ) {
|
|
|
|
tableOfContents.expandSection( hashSection.id );
|
|
|
|
}
|
|
|
|
|
|
|
|
// T325086: If hash fragment corresponds to a section AND the user is at
|
|
|
|
// bottom of page, activate the section. Otherwise, use section observer to
|
|
|
|
// calculate the active section.
|
|
|
|
//
|
|
|
|
// Note that even if a hash fragment is present, it's possible for the
|
|
|
|
// browser to scroll to a position that is different from the position of
|
|
|
|
// the section that corresponds to the hash fragment. This can happen when
|
|
|
|
// the browser remembers a prior scroll position after refreshing the page,
|
|
|
|
// for example.
|
|
|
|
if (
|
|
|
|
hashSection &&
|
|
|
|
Math.round( window.innerHeight + window.scrollY ) >= document.body.scrollHeight
|
|
|
|
) {
|
|
|
|
tableOfContents.changeActiveSection( hashSection.id );
|
|
|
|
} else {
|
|
|
|
// Fallback to section observer's calculation for the active section.
|
|
|
|
sectionObserver.calcIntersection();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
setInitialActiveSection();
|
|
|
|
|
2023-02-18 00:31:56 +00:00
|
|
|
return tableOfContents;
|
|
|
|
};
|
|
|
|
|
2021-10-20 18:58:49 +00:00
|
|
|
/**
|
|
|
|
* @return {void}
|
|
|
|
*/
|
|
|
|
const main = () => {
|
2023-02-16 01:20:00 +00:00
|
|
|
const isIntersectionObserverSupported = 'IntersectionObserver' in window;
|
|
|
|
|
2023-02-18 00:31:56 +00:00
|
|
|
//
|
|
|
|
// Table of contents
|
|
|
|
//
|
2023-02-16 01:20:00 +00:00
|
|
|
const tocElement = document.getElementById( TOC_ID );
|
|
|
|
const bodyContent = document.getElementById( BODY_CONTENT_ID );
|
|
|
|
|
|
|
|
const isToCUpdatingAllowed = isIntersectionObserverSupported &&
|
|
|
|
window.requestAnimationFrame;
|
|
|
|
const tableOfContents = isToCUpdatingAllowed ?
|
2023-02-28 19:48:31 +00:00
|
|
|
setupTableOfContents( tocElement, bodyContent, initSectionObserver ) : null;
|
2023-02-18 00:31:56 +00:00
|
|
|
|
2022-07-01 20:19:57 +00:00
|
|
|
//
|
2022-03-23 20:35:27 +00:00
|
|
|
// Sticky header
|
2022-07-01 20:19:57 +00:00
|
|
|
//
|
2022-03-23 20:35:27 +00:00
|
|
|
const
|
2023-03-01 17:23:43 +00:00
|
|
|
stickyHeaderElement = document.getElementById( stickyHeader.STICKY_HEADER_ID ),
|
2022-03-23 20:35:27 +00:00
|
|
|
stickyIntersection = document.getElementById( stickyHeader.FIRST_HEADING_ID ),
|
2023-02-16 17:06:22 +00:00
|
|
|
userLinksDropdown = document.getElementById( stickyHeader.USER_LINKS_DROPDOWN_ID ),
|
2022-03-23 20:35:27 +00:00
|
|
|
allowedNamespace = stickyHeader.isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ),
|
|
|
|
allowedAction = stickyHeader.isAllowedAction( mw.config.get( 'wgAction' ) );
|
|
|
|
|
|
|
|
const isStickyHeaderAllowed =
|
2023-03-01 17:23:43 +00:00
|
|
|
!!stickyHeaderElement &&
|
2022-03-23 20:35:27 +00:00
|
|
|
!!stickyIntersection &&
|
2023-02-16 17:06:22 +00:00
|
|
|
!!userLinksDropdown &&
|
2022-03-23 20:35:27 +00:00
|
|
|
allowedNamespace &&
|
|
|
|
allowedAction &&
|
2023-02-16 01:20:00 +00:00
|
|
|
isIntersectionObserverSupported;
|
2022-03-23 20:35:27 +00:00
|
|
|
|
2022-04-14 03:58:05 +00:00
|
|
|
const { showStickyHeader, disableEditIcons } = initStickyHeaderABTests(
|
|
|
|
ABTestConfig,
|
|
|
|
isStickyHeaderAllowed && !mw.user.isAnon(),
|
|
|
|
( config ) => initExperiment(
|
2023-05-11 19:03:15 +00:00
|
|
|
config,
|
|
|
|
String( mw.user.getId() )
|
2022-04-14 03:58:05 +00:00
|
|
|
)
|
|
|
|
);
|
2022-05-27 16:36:17 +00:00
|
|
|
|
2022-07-01 20:19:57 +00:00
|
|
|
// Set up intersection observer for page title
|
|
|
|
// Used to show/hide sticky header and add class used by collapsible TOC (T307900)
|
2021-10-26 23:37:56 +00:00
|
|
|
const observer = scrollObserver.initScrollObserver(
|
|
|
|
() => {
|
2022-04-14 03:58:05 +00:00
|
|
|
if ( isStickyHeaderAllowed && showStickyHeader ) {
|
2022-05-05 20:54:20 +00:00
|
|
|
stickyHeader.show();
|
2022-07-01 20:19:57 +00:00
|
|
|
updateTocLocation();
|
2021-12-01 22:31:48 +00:00
|
|
|
}
|
2022-07-01 20:19:57 +00:00
|
|
|
document.body.classList.add( PAGE_TITLE_INTERSECTION_CLASS );
|
2023-02-14 18:01:14 +00:00
|
|
|
if ( tableOfContents ) {
|
|
|
|
tableOfContents.updateTocToggleStyles( true );
|
|
|
|
}
|
2023-02-14 20:33:36 +00:00
|
|
|
scrollObserver.firePageTitleScrollHook( 'down' );
|
2021-10-26 23:37:56 +00:00
|
|
|
},
|
|
|
|
() => {
|
2022-04-14 03:58:05 +00:00
|
|
|
if ( isStickyHeaderAllowed && showStickyHeader ) {
|
2022-05-05 20:54:20 +00:00
|
|
|
stickyHeader.hide();
|
2022-07-01 20:19:57 +00:00
|
|
|
updateTocLocation();
|
2021-12-01 22:31:48 +00:00
|
|
|
}
|
2022-07-01 20:19:57 +00:00
|
|
|
document.body.classList.remove( PAGE_TITLE_INTERSECTION_CLASS );
|
2023-02-14 18:01:14 +00:00
|
|
|
if ( tableOfContents ) {
|
|
|
|
tableOfContents.updateTocToggleStyles( false );
|
|
|
|
}
|
2023-02-14 20:33:36 +00:00
|
|
|
scrollObserver.firePageTitleScrollHook( 'up' );
|
2021-10-26 23:37:56 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-07-01 20:19:57 +00:00
|
|
|
// Handle toc location when sticky header is hidden on lower viewports
|
|
|
|
belowDesktopMedia.onchange = () => {
|
|
|
|
updateTocLocation();
|
|
|
|
};
|
|
|
|
|
2022-12-09 21:27:12 +00:00
|
|
|
updateTocLocation();
|
|
|
|
|
2022-05-31 22:33:31 +00:00
|
|
|
if ( !showStickyHeader ) {
|
|
|
|
stickyHeader.hide();
|
|
|
|
}
|
|
|
|
|
2022-04-14 03:58:05 +00:00
|
|
|
if ( isStickyHeaderAllowed && showStickyHeader ) {
|
2022-03-23 20:35:27 +00:00
|
|
|
stickyHeader.initStickyHeader( {
|
2023-03-01 17:23:43 +00:00
|
|
|
header: stickyHeaderElement,
|
2023-02-16 17:06:22 +00:00
|
|
|
userLinksDropdown,
|
2022-03-23 20:35:27 +00:00
|
|
|
observer,
|
2022-04-14 03:58:05 +00:00
|
|
|
stickyIntersection,
|
|
|
|
disableEditIcons
|
2022-03-23 20:35:27 +00:00
|
|
|
} );
|
|
|
|
} else if ( stickyIntersection ) {
|
|
|
|
observer.observe( stickyIntersection );
|
2021-12-01 22:31:48 +00:00
|
|
|
}
|
2021-10-20 19:10:42 +00:00
|
|
|
};
|
2021-10-20 18:58:49 +00:00
|
|
|
|
2021-10-30 01:01:36 +00:00
|
|
|
module.exports = {
|
2022-03-17 16:54:26 +00:00
|
|
|
main,
|
|
|
|
test: {
|
2023-02-16 01:20:00 +00:00
|
|
|
setupTableOfContents,
|
2022-04-14 03:58:05 +00:00
|
|
|
initStickyHeaderABTests,
|
2022-03-17 16:54:26 +00:00
|
|
|
getHeadingIntersectionHandler
|
|
|
|
}
|
2021-10-30 01:01:36 +00:00
|
|
|
};
|