/** * Enhance dropdown menus * Based on Vector */ const DROPDOWN_CONTAINER_SELECTOR = '.citizen-dropdown', DROPDOWN_DETAILS_SELECTOR = '.citizen-dropdown-details', DROPDOWN_SUMMARY_SELECTOR = '.citizen-dropdown-summary', DROPDOWN_TARGET_SELECTOR = '.citizen-menu__card'; const isPointerDevice = window.matchMedia( '(hover: hover) and (pointer: fine)' ).matches; /** * Represents a Dropdown menu with enhanced functionality. * This class handles the behavior of a dropdown menu, including dismissing the menu when clicking outside, * pressing the ESCAPE key, losing focus, or clicking on links. * It provides methods to bind and unbind event listeners for different actions. * The 'init' method initializes the dropdown menu by adding necessary event listeners. * * @class */ class Dropdown { constructor( details, summary, target ) { this.details = details; this.summary = summary; this.target = target; this.dismissOnEscape = this.dismissOnEscape.bind( this ); this.dismissIfExternalEventTarget = this.dismissIfExternalEventTarget.bind( this ); this.dismissOnLinkClick = this.dismissOnLinkClick.bind( this ); this.onDetailsToggle = this.onDetailsToggle.bind( this ); } dismiss() { if ( this.details && this.details.open ) { this.details.open = false; } } /** * Dismiss the target when ESCAPE is pressed. * * @param {Event} event */ dismissOnEscape( event ) { if ( event.key === 'Escape' ) { this.dismiss(); } } /** * Dismiss the target when event is outside the target and summary * * @param {Event} event */ dismissIfExternalEventTarget( event ) { if ( !this.target.contains( event.target ) && !this.summary.contains( event.target ) ) { this.dismiss(); } } /** * Dismiss the target on clicks to links and link children elements * * @param {Event} event */ dismissOnLinkClick( event ) { const eventTarget = event.target; if ( eventTarget && eventTarget.closest( 'a' ) ) { this.dismiss(); } } /** * Unbind event listeners for the dropdown menu. */ unbind() { this.target.removeEventListener( 'click', this.dismissOnLinkClick ); window.removeEventListener( 'mousedown', this.dismissIfExternalEventTarget ); window.removeEventListener( 'touchstart', this.dismissIfExternalEventTarget ); window.removeEventListener( 'focusin', this.dismissIfExternalEventTarget ); window.removeEventListener( 'keyup', this.dismissOnEscape ); } /** * Bind event listeners for the dropdown menu. */ bind() { this.target.addEventListener( 'click', this.dismissOnLinkClick ); window.addEventListener( 'mousedown', this.dismissIfExternalEventTarget ); window.addEventListener( 'touchstart', this.dismissIfExternalEventTarget, { passive: true } ); window.addEventListener( 'focusin', this.dismissIfExternalEventTarget ); window.addEventListener( 'keyup', this.dismissOnEscape ); } onDetailsToggle() { if ( this.details.open ) { this.bind(); } else { this.unbind(); } } addKeyhint() { if ( !isPointerDevice ) { return; } const links = this.target.querySelectorAll( '.mw-list-item > a[accesskey]' ); links.forEach( ( link ) => { const keyhint = document.createElement( 'span' ); keyhint.classList.add( 'citizen-keyboard-hint-key' ); keyhint.innerText = window.jQuery.fn.updateTooltipAccessKeys.getAccessKeyPrefix() + link.getAttribute( 'accesskey' ); link.append( keyhint ); } ); } init() { this.details.addEventListener( 'toggle', this.onDetailsToggle ); // T295085: Close all dropdown menus when page is unloaded to prevent them // from being open when navigating back to a page. window.addEventListener( 'beforeunload', () => this.dismiss ); this.addKeyhint(); } } function init() { const dropdowns = document.querySelectorAll( DROPDOWN_CONTAINER_SELECTOR ); dropdowns.forEach( ( dropdown ) => { const details = dropdown.querySelector( DROPDOWN_DETAILS_SELECTOR ), summary = dropdown.querySelector( DROPDOWN_SUMMARY_SELECTOR ), target = dropdown.querySelector( DROPDOWN_TARGET_SELECTOR ); if ( !( details && summary && target ) ) { return; } new Dropdown( details, summary, target ).init(); } ); } module.exports = { init: init };