mediawiki-skins-Citizen/resources/skins.citizen.scripts/stickyHeader.js
alistair3149 c9d98e477a
feat(stickyHeader): add CSS var --height-sticky-header to offset sticky elements
--height-sticky-header will always return the current height of sticky header.
It can be used to offset sticky elements so that it adjusts to the sticky header.
2024-07-19 17:08:03 -04:00

105 lines
3.3 KiB
JavaScript

const SCROLL_DOWN_CLASS = 'citizen-scroll--down';
const SCROLL_UP_CLASS = 'citizen-scroll--up';
const STICKY_CLASS = 'citizen-page-header--sticky';
const { initDirectionObserver, initIntersectionObserver } = require( './scrollObserver.js' );
/**
* Observes the scroll direction and adds/removes corresponding classes to the body element.
*
* @return {void}
*/
function observeScrollDirection() {
const toggleScrollClass = ( removeClass, addClass ) => () => {
window.requestAnimationFrame( () => {
document.body.classList.remove( removeClass );
document.body.classList.add( addClass );
} );
};
const addScrollDownClass = toggleScrollClass( SCROLL_UP_CLASS, SCROLL_DOWN_CLASS );
const addScrollUpClass = toggleScrollClass( SCROLL_DOWN_CLASS, SCROLL_UP_CLASS );
initDirectionObserver( addScrollDownClass, addScrollUpClass, 50 );
}
/**
* Initializes the sticky header functionality for Citizen
*
* @return {void}
*/
function init() {
observeScrollDirection();
const header = document.querySelector( '.citizen-page-header' );
const sentinel = document.createElement( 'div' );
sentinel.id = 'citizen-page-header-sticky-sentinel';
header.insertAdjacentElement( 'afterend', sentinel );
const shouldStickyHeader = getComputedStyle( sentinel ).getPropertyValue( 'display' ) !== 'none';
if ( !shouldStickyHeader ) {
return;
}
const placeholder = document.createElement( 'div' );
placeholder.id = 'citizen-page-header-sticky-placeholder';
header.insertAdjacentElement( 'afterend', placeholder );
let staticHeaderHeight = header.getBoundingClientRect().height;
let stickyHeaderHeight = 0;
let placeholderHeight = 0;
let shouldRecalcHeight = true;
const toggleStickyHeader = ( isSticky ) => {
window.requestAnimationFrame( () => {
if ( !shouldRecalcHeight ) {
// The previous height is valid, set the height first
placeholder.style.height = isSticky ? `${ placeholderHeight }px` : '0px';
document.body.classList.toggle( STICKY_CLASS, isSticky );
} else {
// The previous height is invalid, need to set to sticky to get the sticky height
document.body.classList.toggle( STICKY_CLASS, isSticky );
if ( isSticky ) {
stickyHeaderHeight = header.getBoundingClientRect().height;
placeholderHeight = staticHeaderHeight - stickyHeaderHeight;
placeholder.style.height = `${ placeholderHeight }px`;
shouldRecalcHeight = false;
}
placeholder.style.height = `${ isSticky ? placeholderHeight : 0 }px`;
}
// Update sticky header CSS variable, used by other sticky elements
document.documentElement.style.setProperty(
'--height-sticky-header',
`${ isSticky ? stickyHeaderHeight : 0 }px`
);
} );
};
const onResize = () => {
toggleStickyHeader( false );
};
const onResizeEnd = mw.util.debounce( () => {
// Refresh static header height after resize
staticHeaderHeight = header.getBoundingClientRect().height;
shouldRecalcHeight = true;
toggleStickyHeader( true );
}, 250 );
const observer = initIntersectionObserver(
() => {
toggleStickyHeader( true );
window.addEventListener( 'resize', onResize );
window.addEventListener( 'resize', onResizeEnd );
},
() => {
toggleStickyHeader( false );
window.removeEventListener( 'resize', onResize );
window.removeEventListener( 'resize', onResizeEnd );
}
);
observer.observe( sentinel );
}
module.exports = {
init: init
};