2021-09-07 19:48:23 +00:00
|
|
|
var
|
|
|
|
STICKY_HEADER_ID = 'vector-sticky-header',
|
2021-09-09 02:26:33 +00:00
|
|
|
STICKY_HEADER_APPENDED_ID = '-sticky-header',
|
2021-09-07 19:48:23 +00:00
|
|
|
STICKY_HEADER_VISIBLE_CLASS = 'vector-sticky-header-visible',
|
2021-09-09 02:26:33 +00:00
|
|
|
STICKY_HEADER_USER_MENU_CONTAINER_CLASS = 'vector-sticky-header-icon-end',
|
|
|
|
FIRST_HEADING_ID = 'firstHeading',
|
|
|
|
USER_MENU_ID = 'p-personal',
|
|
|
|
VECTOR_USER_LINKS_SELECTOR = '.vector-user-links',
|
2021-09-09 21:40:06 +00:00
|
|
|
VECTOR_MENU_CONTENT_LIST_SELECTOR = '.vector-menu-content-list',
|
|
|
|
SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle';
|
2021-09-07 19:48:23 +00:00
|
|
|
|
2021-09-14 16:53:35 +00:00
|
|
|
/**
|
|
|
|
* Copies attribute from an element to another.
|
|
|
|
*
|
|
|
|
* @param {Element} from
|
|
|
|
* @param {Element} to
|
|
|
|
* @param {string} attribute
|
|
|
|
*/
|
|
|
|
function copyAttribute( from, to, attribute ) {
|
|
|
|
var fromAttr = from.getAttribute( attribute );
|
|
|
|
if ( fromAttr ) {
|
|
|
|
to.setAttribute( attribute, fromAttr );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-14 19:32:09 +00:00
|
|
|
/**
|
|
|
|
* Suffixes an attribute with a value that indicates it
|
|
|
|
* relates to the sticky header to support click tracking instrumentation.
|
|
|
|
*
|
|
|
|
* @param {Element} node
|
|
|
|
* @param {string} attribute
|
|
|
|
*/
|
|
|
|
function suffixStickyAttribute( node, attribute ) {
|
|
|
|
var value = node.getAttribute( attribute );
|
|
|
|
if ( value ) {
|
|
|
|
node.setAttribute( attribute, value + STICKY_HEADER_APPENDED_ID );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Makes a node trackable by our click tracking instrumentation.
|
|
|
|
*
|
|
|
|
* @param {Element} node
|
|
|
|
*/
|
|
|
|
function makeNodeTrackable( node ) {
|
|
|
|
suffixStickyAttribute( node, 'id' );
|
|
|
|
suffixStickyAttribute( node, 'data-event-name' );
|
|
|
|
}
|
|
|
|
|
2021-09-14 16:53:35 +00:00
|
|
|
/**
|
|
|
|
* Makes sticky header icons functional for modern Vector.
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} header
|
|
|
|
* @param {HTMLElement|null} history
|
|
|
|
* @param {HTMLElement|null} talk
|
|
|
|
*/
|
|
|
|
function prepareIcons( header, history, talk ) {
|
|
|
|
var historySticky = header.querySelector( '#ca-history-sticky-header' ),
|
|
|
|
talkSticky = header.querySelector( '#ca-talk-sticky-header' );
|
|
|
|
|
|
|
|
if ( !historySticky || !talkSticky ) {
|
|
|
|
throw new Error( 'Sticky header has unexpected HTML' );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( history ) {
|
|
|
|
copyAttribute( history, historySticky, 'href' );
|
|
|
|
} else {
|
|
|
|
// @ts-ignore
|
|
|
|
historySticky.parentNode.removeChild( historySticky );
|
|
|
|
}
|
|
|
|
if ( talk ) {
|
|
|
|
copyAttribute( talk, talkSticky, 'href' );
|
|
|
|
} else {
|
|
|
|
// @ts-ignore
|
|
|
|
talkSticky.parentNode.removeChild( talkSticky );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-07 19:48:23 +00:00
|
|
|
/**
|
|
|
|
* Makes sticky header functional for modern Vector.
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} header
|
|
|
|
* @param {HTMLElement} stickyIntersection
|
2021-09-09 02:26:33 +00:00
|
|
|
* @param {HTMLElement} userMenu
|
2021-09-14 19:24:26 +00:00
|
|
|
* @param {Element} userMenuStickyContainer
|
2021-09-07 19:48:23 +00:00
|
|
|
*/
|
2021-09-09 02:26:33 +00:00
|
|
|
function makeStickyHeaderFunctional(
|
|
|
|
header,
|
|
|
|
stickyIntersection,
|
|
|
|
userMenu,
|
|
|
|
userMenuStickyContainer
|
|
|
|
) {
|
2021-09-07 19:48:23 +00:00
|
|
|
/* eslint-disable-next-line compat/compat */
|
2021-09-09 02:26:33 +00:00
|
|
|
var
|
|
|
|
stickyObserver = new IntersectionObserver( function ( entries ) {
|
|
|
|
if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) {
|
|
|
|
// Viewport has crossed the bottom edge of firstHeading so show sticky header.
|
|
|
|
// eslint-disable-next-line mediawiki/class-doc
|
|
|
|
header.classList.add( STICKY_HEADER_VISIBLE_CLASS );
|
|
|
|
} else {
|
|
|
|
// Viewport is above the bottom edge of firstHeading so hide sticky header.
|
|
|
|
// eslint-disable-next-line mediawiki/class-doc
|
|
|
|
header.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
|
|
|
|
}
|
|
|
|
} ),
|
2021-09-14 19:24:26 +00:00
|
|
|
// Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518
|
|
|
|
userMenuClone = /** @type {HTMLElement} */( userMenu.cloneNode( true ) ),
|
2021-09-09 02:26:33 +00:00
|
|
|
userMenuStickyElementsWithIds = userMenuClone.querySelectorAll( '[ id ], [ data-event-name ]' ),
|
2021-09-14 19:24:26 +00:00
|
|
|
userMenuStickyContainerInner = userMenuStickyContainer.querySelector( VECTOR_USER_LINKS_SELECTOR );
|
2021-09-09 02:26:33 +00:00
|
|
|
|
|
|
|
// Update all ids of the cloned user menu to make them unique.
|
2021-09-14 19:32:09 +00:00
|
|
|
makeNodeTrackable( userMenuClone );
|
|
|
|
userMenuStickyElementsWithIds.forEach( makeNodeTrackable );
|
2021-09-09 02:26:33 +00:00
|
|
|
|
|
|
|
// Add gadget-injected items of the fixed user menu into the sticky header user menu.
|
|
|
|
// Only applies to gadgets running after the code above and won't apply to existing items.
|
|
|
|
mw.hook( 'util.addPortletLink' ).add( function ( /** @type {HTMLElement} */ item ) {
|
|
|
|
// Get the nav tag parent of the gadget-injected menu item. We verify that .closest is
|
|
|
|
// available for use because of feature detection in init function.
|
2021-09-14 19:24:26 +00:00
|
|
|
var parentNav = item.closest( 'nav' );
|
2021-09-09 02:26:33 +00:00
|
|
|
// Check if a gadget is injecting an item into the user menu.
|
2021-09-14 19:24:26 +00:00
|
|
|
if ( parentNav && parentNav.getAttribute( 'id' ) === 'p-personal' ) {
|
2021-09-09 02:26:33 +00:00
|
|
|
var
|
|
|
|
itemClone = /** @type {HTMLElement} */ ( item.cloneNode( true ) ),
|
2021-09-14 19:24:26 +00:00
|
|
|
userMenuCloneUl = userMenuClone.querySelector( VECTOR_MENU_CONTENT_LIST_SELECTOR );
|
2021-09-09 02:26:33 +00:00
|
|
|
if ( userMenuCloneUl ) {
|
2021-09-14 19:32:09 +00:00
|
|
|
makeNodeTrackable( itemClone );
|
2021-09-09 02:26:33 +00:00
|
|
|
userMenuCloneUl.appendChild( itemClone );
|
|
|
|
}
|
2021-09-07 19:48:23 +00:00
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
2021-09-09 02:26:33 +00:00
|
|
|
// Clone the updated user menu to the sticky header.
|
2021-09-14 19:24:26 +00:00
|
|
|
if ( userMenuStickyContainerInner ) {
|
|
|
|
userMenuStickyContainerInner.appendChild( userMenuClone );
|
|
|
|
}
|
2021-09-09 02:26:33 +00:00
|
|
|
|
2021-09-14 16:53:35 +00:00
|
|
|
prepareIcons( header,
|
|
|
|
document.querySelector( '#ca-history a' ),
|
|
|
|
document.querySelector( '#ca-talk a' )
|
|
|
|
);
|
2021-09-07 19:48:23 +00:00
|
|
|
stickyObserver.observe( stickyIntersection );
|
|
|
|
}
|
|
|
|
|
2021-09-09 21:40:06 +00:00
|
|
|
/**
|
|
|
|
* @param {HTMLElement} header
|
|
|
|
*/
|
|
|
|
function setupSearchIfNeeded( header ) {
|
|
|
|
var
|
|
|
|
searchToggle = header.querySelector( SEARCH_TOGGLE_SELECTOR );
|
|
|
|
|
|
|
|
if ( !(
|
|
|
|
searchToggle &&
|
|
|
|
window.fetch &&
|
|
|
|
document.body.classList.contains( 'skin-vector-search-vue' )
|
|
|
|
) ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the `skins.vector.search` module here or setup an event handler to
|
|
|
|
// load it depending on the outcome of T289718. After it loads, initialize the
|
|
|
|
// search toggle.
|
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
// mw.loader.using( 'skins.vector.search', function () {
|
|
|
|
// initSearchToggle( searchToggle );
|
|
|
|
// } );
|
|
|
|
}
|
|
|
|
|
2021-09-07 19:48:23 +00:00
|
|
|
module.exports = function initStickyHeader() {
|
2021-09-14 19:24:26 +00:00
|
|
|
var header = document.getElementById( STICKY_HEADER_ID ),
|
|
|
|
stickyIntersection = document.getElementById(
|
2021-09-07 19:48:23 +00:00
|
|
|
FIRST_HEADING_ID
|
2021-09-14 19:24:26 +00:00
|
|
|
),
|
|
|
|
userMenu = document.getElementById( USER_MENU_ID ),
|
|
|
|
userMenuStickyContainer = document.getElementsByClassName(
|
|
|
|
STICKY_HEADER_USER_MENU_CONTAINER_CLASS
|
|
|
|
)[ 0 ];
|
2021-09-07 19:48:23 +00:00
|
|
|
|
|
|
|
if ( !(
|
|
|
|
header &&
|
2021-09-09 02:26:33 +00:00
|
|
|
header.closest &&
|
|
|
|
stickyIntersection &&
|
|
|
|
userMenu &&
|
|
|
|
userMenuStickyContainer &&
|
2021-09-07 19:48:23 +00:00
|
|
|
'IntersectionObserver' in window ) ) {
|
2021-08-30 22:44:00 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-09-07 19:48:23 +00:00
|
|
|
|
2021-09-09 02:26:33 +00:00
|
|
|
makeStickyHeaderFunctional( header, stickyIntersection, userMenu, userMenuStickyContainer );
|
2021-09-09 21:40:06 +00:00
|
|
|
setupSearchIfNeeded( header );
|
2021-08-30 22:44:00 +00:00
|
|
|
};
|