Move sticky header DOM queries into main.js

- Remove isStickyHeaderAllowed() from stickyHeader.js, move to main.js
- Rename variables in stickyHeader.js to be consistent

Bug: T301429
Change-Id: Ib445a19cbfab52a008b749ea63cef178d6288e6a
This commit is contained in:
bwang 2022-03-23 15:35:27 -05:00
parent 3590f4aac9
commit 607f3279bd
2 changed files with 102 additions and 98 deletions

View File

@ -52,12 +52,30 @@ const main = () => {
searchToggle( searchToggleElement );
}
// If necessary, initialize experiment and fire the A/B test enrollment hook.
const stickyHeaderExperiment =
// Sticky header
const
header = document.getElementById( stickyHeader.STICKY_HEADER_ID ),
stickyIntersection = document.getElementById( stickyHeader.FIRST_HEADING_ID ),
userMenu = document.getElementById( stickyHeader.USER_MENU_ID ),
allowedNamespace = stickyHeader.isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ),
allowedAction = stickyHeader.isAllowedAction( mw.config.get( 'wgAction' ) );
const isStickyHeaderAllowed =
!!header &&
!!stickyIntersection &&
!!userMenu &&
allowedNamespace &&
allowedAction &&
'IntersectionObserver' in window;
const isExperimentEnabled =
!!ABTestConfig.enabled &&
ABTestConfig.name === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME &&
!mw.user.isAnon() &&
stickyHeader.isStickyHeaderAllowed() &&
isStickyHeaderAllowed;
// If necessary, initialize experiment and fire the A/B test enrollment hook.
const stickyHeaderExperiment = isExperimentEnabled &&
initExperiment( Object.assign( {}, ABTestConfig, { token: mw.user.getId() } ) );
// Remove class if present on the html element so that scroll padding isn't undesirably
@ -66,11 +84,9 @@ const main = () => {
document.documentElement.classList.remove( 'vector-sticky-header-enabled' );
}
const
targetElement = stickyHeader.header,
targetIntersection = stickyHeader.stickyIntersection,
isStickyHeaderAllowed = stickyHeaderExperiment ?
stickyHeaderExperiment.isInTreatmentBucket() : stickyHeader.isStickyHeaderAllowed();
const isStickyHeaderEnabled = stickyHeaderExperiment ?
stickyHeaderExperiment.isInTreatmentBucket() :
isStickyHeaderAllowed;
// Table of contents
const tocElement = document.getElementById( TOC_ID );
@ -84,8 +100,8 @@ const main = () => {
// either feature.
const observer = scrollObserver.initScrollObserver(
() => {
if ( targetElement && isStickyHeaderAllowed ) {
stickyHeader.show();
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) {
stickyHeader.show( header );
}
scrollObserver.fireScrollHook( 'down', PAGE_TITLE_SCROLL_HOOK );
if ( tocLegacyTargetIntersection ) {
@ -93,22 +109,25 @@ const main = () => {
}
},
() => {
if ( targetElement && isStickyHeaderAllowed ) {
stickyHeader.hide();
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) {
stickyHeader.hide( header );
}
scrollObserver.fireScrollHook( 'up', PAGE_TITLE_SCROLL_HOOK );
if ( tocLegacyTargetIntersection ) {
scrollObserver.fireScrollHook( 'up', TOC_SCROLL_HOOK );
}
}
);
// Initiate observer for sticky header.
if ( isStickyHeaderAllowed ) {
stickyHeader.initStickyHeader( observer );
} else if ( targetIntersection ) {
observer.observe( targetIntersection );
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) {
stickyHeader.initStickyHeader( {
header,
userMenu,
observer,
stickyIntersection
} );
} else if ( stickyIntersection ) {
observer.observe( stickyIntersection );
}
// Initiate observer for table of contents in main content.
@ -169,7 +188,7 @@ const main = () => {
} );
const sectionObserver = initSectionObserver( {
elements: bodyContent.querySelectorAll( 'h1, h2, h3, h4, h5, h6, .mw-body-content' ),
topMargin: targetElement ? targetElement.getBoundingClientRect().height : 0,
topMargin: header ? header.getBoundingClientRect().height : 0,
onIntersection: getHeadingIntersectionHandler( tableOfContents.changeActiveSection )
} );
};

View File

@ -2,9 +2,8 @@
* Functions and variables to implement sticky header.
*/
const
STICKY_HEADER_ID = 'vector-sticky-header',
header = document.getElementById( STICKY_HEADER_ID ),
initSearchToggle = require( './searchToggle.js' ),
STICKY_HEADER_ID = 'vector-sticky-header',
STICKY_HEADER_APPENDED_ID = '-sticky-header',
STICKY_HEADER_VISIBLE_CLASS = 'vector-sticky-header-visible',
STICKY_HEADER_USER_MENU_CONTAINER_CLASS = 'vector-sticky-header-icon-end',
@ -32,22 +31,22 @@ function copyAttribute( from, to, attribute ) {
/**
* Show the sticky header.
*
* @param {Element} header
*/
function show() {
if ( header ) {
header.classList.add( STICKY_HEADER_VISIBLE_CLASS );
}
function show( header ) {
header.classList.add( STICKY_HEADER_VISIBLE_CLASS );
document.body.classList.remove( ULS_HIDE_CLASS );
}
/**
* Hide the sticky header.
*
* @param {Element} header
*/
function hide() {
if ( header ) {
header.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
document.body.classList.add( ULS_HIDE_CLASS );
}
function hide( header ) {
header.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
document.body.classList.add( ULS_HIDE_CLASS );
}
/**
@ -145,15 +144,15 @@ function watchstarCallback( $link, isWatched, expiry ) {
/**
* Makes sticky header icons functional for modern Vector.
*
* @param {Element} headerElement
* @param {Element} header
* @param {Element|null} history
* @param {Element|null} talk
* @param {Element|null} watch
*/
function prepareIcons( headerElement, history, talk, watch ) {
const historySticky = headerElement.querySelector( '#ca-history-sticky-header' ),
talkSticky = headerElement.querySelector( '#ca-talk-sticky-header' ),
watchSticky = headerElement.querySelector( '#ca-watchstar-sticky-header' );
function prepareIcons( header, history, talk, watch ) {
const historySticky = header.querySelector( '#ca-history-sticky-header' ),
talkSticky = header.querySelector( '#ca-talk-sticky-header' ),
watchSticky = header.querySelector( '#ca-watchstar-sticky-header' );
if ( !historySticky || !talkSticky || !watchSticky ) {
throw new Error( 'Sticky header has unexpected HTML' );
@ -188,7 +187,7 @@ function prepareIcons( headerElement, history, talk, watch ) {
/**
* Render sticky header edit or protected page icons for modern Vector.
*
* @param {Element} headerElement
* @param {Element} header
* @param {Element|null} primaryEdit
* @param {boolean} isProtected
* @param {Element|null} secondaryEdit
@ -196,20 +195,20 @@ function prepareIcons( headerElement, history, talk, watch ) {
* header.
*/
function prepareEditIcons(
headerElement,
header,
primaryEdit,
isProtected,
secondaryEdit,
disableStickyHeader
) {
const
primaryEditSticky = headerElement.querySelector(
primaryEditSticky = header.querySelector(
'#ca-ve-edit-sticky-header'
),
protectedSticky = headerElement.querySelector(
protectedSticky = header.querySelector(
'#ca-viewsource-sticky-header'
),
wikitextSticky = headerElement.querySelector(
wikitextSticky = header.querySelector(
'#ca-edit-sticky-header'
);
@ -285,14 +284,15 @@ function isInViewport( element ) {
/**
* Add hooks for sticky header when Visual Editor is used.
*
* @param {Element} targetIntersection intersection element
* @param {Element} header
* @param {Element} stickyIntersection intersection element
* @param {IntersectionObserver} observer
*/
function addVisualEditorHooks( targetIntersection, observer ) {
function addVisualEditorHooks( header, stickyIntersection, observer ) {
// When Visual Editor is activated, hide the sticky header.
mw.hook( 've.activationStart' ).add( () => {
hide();
observer.unobserve( targetIntersection );
hide( header );
observer.unobserve( stickyIntersection );
} );
// When Visual Editor is deactivated (by clicking "Read" tab at top of page), show sticky header
@ -301,15 +301,15 @@ function addVisualEditorHooks( targetIntersection, observer ) {
// Wait for the next repaint or we might calculate that
// sticky header should not be visible (T299114)
requestAnimationFrame( () => {
observer.observe( targetIntersection );
observer.observe( stickyIntersection );
} );
} );
// After saving edits, re-apply the sticky header if the target is not in the viewport.
mw.hook( 'postEdit.afterRemoval' ).add( () => {
if ( !isInViewport( targetIntersection ) ) {
show();
observer.observe( targetIntersection );
if ( !isInViewport( stickyIntersection ) ) {
show( header );
observer.observe( stickyIntersection );
}
} );
}
@ -343,14 +343,14 @@ function prepareUserMenu( userMenu ) {
/**
* Makes sticky header functional for modern Vector.
*
* @param {Element} headerElement
* @param {Element} header
* @param {Element} userMenu
* @param {Element} userMenuStickyContainer
* @param {IntersectionObserver} stickyObserver
* @param {Element} stickyIntersection
*/
function makeStickyHeaderFunctional(
headerElement,
header,
userMenu,
userMenuStickyContainer,
stickyObserver,
@ -365,7 +365,7 @@ function makeStickyHeaderFunctional(
userMenuStickyContainerInner.appendChild( prepareUserMenu( userMenu ) );
}
prepareIcons( headerElement,
prepareIcons( header,
document.querySelector( '#ca-history a' ),
document.querySelector( '#ca-talk a' ),
document.querySelector( '#ca-watch a, #ca-unwatch a' )
@ -378,12 +378,12 @@ function makeStickyHeaderFunctional(
const primaryEdit = protectedEdit || ( veEdit || ceEdit );
const secondaryEdit = veEdit ? ceEdit : null;
const disableStickyHeader = () => {
headerElement.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
header.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
stickyObserver.unobserve( stickyIntersection );
};
prepareEditIcons(
headerElement,
header,
primaryEdit,
isProtected,
secondaryEdit,
@ -394,11 +394,11 @@ function makeStickyHeaderFunctional(
}
/**
* @param {Element} headerElement
* @param {Element} header
*/
function setupSearchIfNeeded( headerElement ) {
function setupSearchIfNeeded( header ) {
const
searchToggle = headerElement.querySelector( SEARCH_TOGGLE_SELECTOR );
searchToggle = header.querySelector( SEARCH_TOGGLE_SELECTOR );
if ( !document.body.classList.contains( 'skin-vector-search-vue' ) ) {
return;
@ -433,49 +433,32 @@ function isAllowedAction( action ) {
return disallowedActions.indexOf( action ) < 0 && !hasDiffId;
}
const
stickyIntersection = document.getElementById(
FIRST_HEADING_ID
),
userMenu = document.getElementById( USER_MENU_ID ),
userMenuStickyContainer = document.getElementsByClassName(
/**
* @typedef {Object} StickyHeaderProps
* @property {Element} header
* @property {Element} userMenu
* @property {IntersectionObserver} observer
* @property {Element} stickyIntersection
*/
/**
* @param {StickyHeaderProps} props
*/
function initStickyHeader( props ) {
const userMenuStickyContainer = document.getElementsByClassName(
STICKY_HEADER_USER_MENU_CONTAINER_CLASS
)[ 0 ],
allowedNamespace = isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ),
allowedAction = isAllowedAction( mw.config.get( 'wgAction' ) );
/**
* Check if all conditions are met to enable sticky header
*
* @return {boolean}
*/
function isStickyHeaderAllowed() {
return !!header &&
!!stickyIntersection &&
!!userMenu &&
userMenuStickyContainer &&
allowedNamespace &&
allowedAction &&
'IntersectionObserver' in window;
}
/**
* @param {IntersectionObserver} observer
*/
function initStickyHeader( observer ) {
if ( !isStickyHeaderAllowed() || !header || !userMenu || !stickyIntersection ) {
return;
}
)[ 0 ];
makeStickyHeaderFunctional(
header,
userMenu,
props.header,
props.userMenu,
userMenuStickyContainer,
observer,
stickyIntersection
props.observer,
props.stickyIntersection
);
setupSearchIfNeeded( header );
addVisualEditorHooks( stickyIntersection, observer );
setupSearchIfNeeded( props.header );
addVisualEditorHooks( props.header, props.stickyIntersection, props.observer );
// Make sure ULS outside sticky header disables the sticky header behaviour.
// @ts-ignore
@ -488,7 +471,7 @@ function initStickyHeader( observer ) {
} );
// Make sure ULS dialog is sticky.
const langBtn = header.querySelector( '#p-lang-btn-sticky-header' );
const langBtn = props.header.querySelector( '#p-lang-btn-sticky-header' );
if ( langBtn ) {
langBtn.addEventListener( 'click', function () {
const bodyClassList = document.body.classList;
@ -502,9 +485,11 @@ module.exports = {
show,
hide,
prepareUserMenu,
isAllowedNamespace,
isAllowedAction,
initStickyHeader,
isStickyHeaderAllowed,
header,
stickyIntersection,
STICKY_HEADER_ID,
FIRST_HEADING_ID,
USER_MENU_ID,
STICKY_HEADER_EXPERIMENT_NAME
};