diff --git a/resources/skins.citizen.scripts/checkboxHack.js b/resources/skins.citizen.scripts/checkboxHack.js index 85d08ac7..c78345b7 100644 --- a/resources/skins.citizen.scripts/checkboxHack.js +++ b/resources/skins.citizen.scripts/checkboxHack.js @@ -13,9 +13,9 @@ * @return {void} * @ignore */ -const updateAriaExpanded = ( checkbox ) => { +function updateAriaExpanded( checkbox ) { checkbox.setAttribute( 'aria-expanded', checkbox.checked.toString() ); -}; +} /** * Set the checked state and fire the 'input' event. @@ -41,7 +41,7 @@ const updateAriaExpanded = ( checkbox ) => { * @return {void} * @ignore */ -const setCheckedState = ( checkbox, checked ) => { +function setCheckedState( checkbox, checked ) { /** @type {Event} @ignore */ let e; checkbox.checked = checked; @@ -57,7 +57,7 @@ const setCheckedState = ( checkbox, checked ) => { e.initCustomEvent( 'input', true /* canBubble */, false, false ); } checkbox.dispatchEvent( e ); -}; +} /** * Returns true if the Event's target is an inclusive descendant of any the checkbox hack's @@ -70,14 +70,13 @@ const setCheckedState = ( checkbox, checked ) => { * @return {boolean} * @ignore */ -const containsEventTarget = ( checkbox, button, target, event ) => { - return ( - event.target instanceof Node && - ( checkbox.contains( event.target ) || - button.contains( event.target ) || - target.contains( event.target ) ) +function containsEventTarget( checkbox, button, target, event ) { + return event.target instanceof Node && ( + checkbox.contains( event.target ) || + button.contains( event.target ) || + target.contains( event.target ) ); -}; +} /** * Dismiss the target when event is outside the checkbox, button, and target. @@ -90,14 +89,11 @@ const containsEventTarget = ( checkbox, button, target, event ) => { * @return {void} * @ignore */ -const dismissIfExternalEventTarget = ( checkbox, button, target, event ) => { - if ( - checkbox.checked && - !containsEventTarget( checkbox, button, target, event ) - ) { +function dismissIfExternalEventTarget( checkbox, button, target, event ) { + if ( checkbox.checked && !containsEventTarget( checkbox, button, target, event ) ) { setCheckedState( checkbox, false ); } -}; +} /** * Update the `aria-expanded` attribute based on checkbox state (target visibility) changes. @@ -106,15 +102,15 @@ const dismissIfExternalEventTarget = ( checkbox, button, target, event ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindUpdateAriaExpandedOnInput = ( checkbox ) => { +function bindUpdateAriaExpandedOnInput( checkbox ) { const listener = updateAriaExpanded.bind( undefined, checkbox ); // Whenever the checkbox state changes, update the `aria-expanded` state. checkbox.addEventListener( 'input', listener ); - return () => { + return function () { checkbox.removeEventListener( 'input', listener ); }; -}; +} /** * Manually change the checkbox state to avoid a focus change when using a pointing device. @@ -124,19 +120,19 @@ const bindUpdateAriaExpandedOnInput = ( checkbox ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindToggleOnClick = ( checkbox, button ) => { - const listener = ( event ) => { +function bindToggleOnClick( checkbox, button ) { + function listener( event ) { // Do not allow the browser to handle the checkbox. Instead, manually toggle it which does // not alter focus. event.preventDefault(); setCheckedState( checkbox, !checkbox.checked ); - }; + } button.addEventListener( 'click', listener, true ); - return () => { + return function () { button.removeEventListener( 'click', listener, true ); }; -}; +} /** * Manually change the checkbox state when the button is focused and Enter is pressed. @@ -145,22 +141,22 @@ const bindToggleOnClick = ( checkbox, button ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindToggleOnEnter = ( checkbox ) => { - const onKeyup = ( { key } ) => { +function bindToggleOnEnter( checkbox ) { + function onKeyup( /** @type {KeyboardEvent} @ignore */ event ) { // Only handle ENTER. - if ( key !== 'Enter' ) { + if ( event.key !== 'Enter' ) { return; } setCheckedState( checkbox, !checkbox.checked ); - }; + } checkbox.addEventListener( 'keyup', onKeyup ); - return () => { + return function () { checkbox.removeEventListener( 'keyup', onKeyup ); }; -}; +} /** * Dismiss the target when clicking elsewhere and update the `aria-expanded` attribute based on @@ -173,19 +169,14 @@ const bindToggleOnEnter = ( checkbox ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindDismissOnClickOutside = ( window, checkbox, button, target ) => { - const listener = dismissIfExternalEventTarget.bind( - undefined, - checkbox, - button, - target - ); +function bindDismissOnClickOutside( window, checkbox, button, target ) { + const listener = dismissIfExternalEventTarget.bind( undefined, checkbox, button, target ); window.addEventListener( 'click', listener, true ); - return () => { + return function () { window.removeEventListener( 'click', listener, true ); }; -}; +} /** * Dismiss the target when focusing elsewhere and update the `aria-expanded` attribute based on @@ -198,21 +189,16 @@ const bindDismissOnClickOutside = ( window, checkbox, button, target ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindDismissOnFocusLoss = ( window, checkbox, button, target ) => { +function bindDismissOnFocusLoss( window, checkbox, button, target ) { // If focus is given to any element outside the target, dismiss the target. Setting a focusout // listener on the target would be preferable, but this interferes with the click listener. - const listener = dismissIfExternalEventTarget.bind( - undefined, - checkbox, - button, - target - ); + const listener = dismissIfExternalEventTarget.bind( undefined, checkbox, button, target ); window.addEventListener( 'focusin', listener, true ); - return () => { + return function () { window.removeEventListener( 'focusin', listener, true ); }; -}; +} /** * Dismiss the target when ESCAPE is pressed. @@ -223,20 +209,20 @@ const bindDismissOnFocusLoss = ( window, checkbox, button, target ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bindDismissOnEscape = ( window, checkbox ) => { - const onKeyup = ( { key } ) => { +function bindDismissOnEscape( window, checkbox ) { + const onKeyup = function ( /** @type {KeyboardEvent} */ event ) { // Only handle ESCAPE - if ( key !== 'Escape' ) { + if ( event.key !== 'Escape' ) { return; } setCheckedState( checkbox, false ); }; window.addEventListener( 'keyup', onKeyup, true ); - return () => { + return function () { window.removeEventListener( 'keyup', onKeyup ); }; -}; +} /** * Dismiss the target when clicking or focusing elsewhere and update the `aria-expanded` attribute @@ -255,7 +241,7 @@ const bindDismissOnEscape = ( window, checkbox ) => { * @return {function(): void} Cleanup function that removes the added event listeners. * @ignore */ -const bind = ( window, checkbox, button, target ) => { +function bind( window, checkbox, button, target ) { const cleanups = [ bindUpdateAriaExpandedOnInput( checkbox ), bindToggleOnClick( checkbox, button ), @@ -265,12 +251,12 @@ const bind = ( window, checkbox, button, target ) => { bindDismissOnEscape( window, checkbox ) ]; - return () => { - cleanups.forEach( ( cleanup ) => { + return function () { + cleanups.forEach( function ( cleanup ) { cleanup(); } ); }; -}; +} module.exports = { updateAriaExpanded: updateAriaExpanded, diff --git a/resources/skins.citizen.scripts/scrollObserver.js b/resources/skins.citizen.scripts/scrollObserver.js index 34c2dc3e..cdde7254 100644 --- a/resources/skins.citizen.scripts/scrollObserver.js +++ b/resources/skins.citizen.scripts/scrollObserver.js @@ -6,12 +6,12 @@ * @param {number} threshold minimum scrolled px to trigger the function * @return {void} */ -const initDirectionObserver = ( onScrollDown, onScrollUp, threshold ) => { +function initDirectionObserver( onScrollDown, onScrollUp, threshold ) { const throttle = require( 'mediawiki.util' ).throttle; let lastScrollTop = window.scrollY; - const onScroll = () => { + function onScroll() { const scrollTop = window.scrollY; if ( Math.abs( scrollTop - lastScrollTop ) < threshold ) { @@ -24,10 +24,10 @@ const initDirectionObserver = ( onScrollDown, onScrollUp, threshold ) => { onScrollUp(); } lastScrollTop = scrollTop; - }; + } window.addEventListener( 'scroll', throttle( onScroll, 250 ) ); -}; +} /** * Create an observer based on element visiblity. @@ -37,9 +37,9 @@ const initDirectionObserver = ( onScrollDown, onScrollUp, threshold ) => { * @param {Function} onVisible functionality for when the element is hidden * @return {IntersectionObserver} */ -const initIntersectionObserver = ( onHidden, onVisible ) => { +function initIntersectionObserver( onHidden, onVisible ) { /* eslint-disable-next-line compat/compat */ - return new IntersectionObserver( ( entries ) => { + return new IntersectionObserver( function ( entries ) { if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) { // Viewport has crossed the bottom edge of the target element. onHidden(); @@ -48,7 +48,7 @@ const initIntersectionObserver = ( onHidden, onVisible ) => { onVisible(); } } ); -}; +} module.exports = { initDirectionObserver, diff --git a/resources/skins.citizen.scripts/search.js b/resources/skins.citizen.scripts/search.js index 1e29a7b7..e59f8055 100644 --- a/resources/skins.citizen.scripts/search.js +++ b/resources/skins.citizen.scripts/search.js @@ -14,18 +14,18 @@ const SEARCH_LOADING_CLASS = 'citizen-loading'; * @param {string} moduleName resourceLoader module to load. * @param {function(): void} afterLoadFn function to execute after search module loads. */ -const loadSearchModule = ( element, moduleName, afterLoadFn ) => { - const requestSearchModule = function requestSearchModule() { +function loadSearchModule( element, moduleName, afterLoadFn ) { + function requestSearchModule() { mw.loader.using( moduleName, afterLoadFn ); element.removeEventListener( 'focus', requestSearchModule ); - }; + } if ( document.activeElement === element ) { requestSearchModule(); } else { element.addEventListener( 'focus', requestSearchModule ); } -}; +} /** * Event callback that shows or hides the loading indicator based on the event type. @@ -36,25 +36,27 @@ const loadSearchModule = ( element, moduleName, afterLoadFn ) => { * * @param {Event} event */ -const renderSearchLoadingIndicator = ( { currentTarget, target, type } ) => { - const form = /** @type {HTMLElement} */ ( currentTarget ); - const input = /** @type {HTMLInputElement} */ ( target ); +function renderSearchLoadingIndicator( event ) { + const form = /** @type {HTMLElement} */ ( event.currentTarget ), + input = /** @type {HTMLInputElement} */ ( event.target ); if ( - !( currentTarget instanceof HTMLElement ) || - !( target instanceof HTMLInputElement ) + !( event.currentTarget instanceof HTMLElement ) || + !( event.target instanceof HTMLInputElement ) ) { return; } - if ( type === 'input' ) { + if ( event.type === 'input' ) { form.classList.add( SEARCH_LOADING_CLASS ); - } else if ( type === 'focusout' ) { + + } else if ( event.type === 'focusout' ) { form.classList.remove( SEARCH_LOADING_CLASS ); - } else if ( type === 'focusin' && input.value.trim() ) { + + } else if ( event.type === 'focusin' && input.value.trim() ) { form.classList.add( SEARCH_LOADING_CLASS ); } -}; +} /** * Attaches or detaches the event listeners responsible for activating @@ -64,20 +66,19 @@ const renderSearchLoadingIndicator = ( { currentTarget, target, type } ) => { * @param {boolean} attach * @param {function(Event): void} eventCallback */ -const setLoadingIndicatorListeners = ( element, attach, eventCallback ) => { +function setLoadingIndicatorListeners( element, attach, eventCallback ) { + /** @type { "addEventListener" | "removeEventListener" } */ - const addOrRemoveListener = attach ? - 'addEventListener' : - 'removeEventListener'; + const addOrRemoveListener = ( attach ? 'addEventListener' : 'removeEventListener' ); - [ 'input', 'focusin', 'focusout' ].forEach( ( eventType ) => { + [ 'input', 'focusin', 'focusout' ].forEach( function ( eventType ) { element[ addOrRemoveListener ]( eventType, eventCallback ); } ); if ( !attach ) { element.classList.remove( SEARCH_LOADING_CLASS ); } -}; +} /** * Manually focus on the input field if checkbox is checked @@ -86,13 +87,13 @@ const setLoadingIndicatorListeners = ( element, attach, eventCallback ) => { * @param {HTMLInputElement} input * @return {void} */ -const focusOnChecked = ( { checked }, input ) => { - if ( checked ) { +function focusOnChecked( checkbox, input ) { + if ( checkbox.checked ) { input.focus(); } else { input.blur(); } -}; +} /** * Check if the element is a HTML form element or content editable @@ -101,23 +102,17 @@ const focusOnChecked = ( { checked }, input ) => { * @param {HTMLElement} element * @return {boolean} */ -const isFormField = ( element ) => { +function isFormField( element ) { if ( !( element instanceof HTMLElement ) ) { return false; } const name = element.nodeName.toLowerCase(); const type = ( element.getAttribute( 'type' ) || '' ).toLowerCase(); - return ( - name === 'select' || - name === 'textarea' || - ( name === 'input' && - type !== 'submit' && - type !== 'reset' && - type !== 'checkbox' && - type !== 'radio' ) || - element.isContentEditable - ); -}; + return ( name === 'select' || + name === 'textarea' || + ( name === 'input' && type !== 'submit' && type !== 'reset' && type !== 'checkbox' && type !== 'radio' ) || + element.isContentEditable ); +} /** * Manually check the checkbox state when the button is SLASH is pressed. @@ -127,8 +122,8 @@ const isFormField = ( element ) => { * @param {HTMLInputElement} input * @return {void} */ -const bindExpandOnSlash = ( window, checkbox, input ) => { - const onExpandOnSlash = /** @type {KeyboardEvent} */ ( event ) => { +function bindExpandOnSlash( window, checkbox, input ) { + const onExpandOnSlash = function ( /** @type {KeyboardEvent} */ event ) { // Only handle SPACE and ENTER. if ( event.key === '/' && !isFormField( event.target ) ) { // Since Firefox quickfind interfere with this @@ -139,23 +134,24 @@ const bindExpandOnSlash = ( window, checkbox, input ) => { }; window.addEventListener( 'keydown', onExpandOnSlash, true ); -}; +} /** * @param {Window} window * @return {void} */ -const initSearch = ( window ) => { - const searchModule = require( './config.json' ).wgCitizenSearchModule; - const searchBoxes = document.querySelectorAll( '.citizen-search-box' ); +function initSearch( window ) { + const + searchModule = require( './config.json' ).wgCitizenSearchModule, + searchBoxes = document.querySelectorAll( '.citizen-search-box' ); if ( !searchBoxes.length ) { return; } - searchBoxes.forEach( ( searchBox ) => { - const input = searchBox.querySelector( 'input[name="search"]' ); - const isPrimarySearch = input && input.getAttribute( 'id' ) === 'searchInput'; + searchBoxes.forEach( function ( searchBox ) { + const + input = searchBox.querySelector( 'input[name="search"]' ), isPrimarySearch = input && input.getAttribute( 'id' ) === 'searchInput'; if ( !input ) { return; @@ -166,21 +162,17 @@ const initSearch = ( window ) => { const checkbox = document.getElementById( 'citizen-search__checkbox' ); bindExpandOnSlash( window, checkbox, input ); // Focus when toggled - checkbox.addEventListener( 'input', () => { + checkbox.addEventListener( 'input', function () { focusOnChecked( checkbox, input ); } ); } setLoadingIndicatorListeners( searchBox, true, renderSearchLoadingIndicator ); - loadSearchModule( input, searchModule, () => { - setLoadingIndicatorListeners( - searchBox, - false, - renderSearchLoadingIndicator - ); + loadSearchModule( input, searchModule, function () { + setLoadingIndicatorListeners( searchBox, false, renderSearchLoadingIndicator ); } ); } ); -}; +} module.exports = { init: initSearch diff --git a/resources/skins.citizen.scripts/sectionObserver.js b/resources/skins.citizen.scripts/sectionObserver.js index 5793bfda..7c6cf9bf 100644 --- a/resources/skins.citizen.scripts/sectionObserver.js +++ b/resources/skins.citizen.scripts/sectionObserver.js @@ -43,32 +43,34 @@ * @param {SectionObserverProps} props * @return {SectionObserver} */ -const sectionObserver = ( props ) => { +function sectionObserver( props ) { props = Object.assign( { topMargin: 0, throttleMs: 200, - onIntersection: () => { } + onIntersection: function () { } }, props ); let /** @type {boolean} */ inThrottle = false; let /** @type {HTMLElement | undefined} */ current; // eslint-disable-next-line compat/compat - const observer = new IntersectionObserver( ( entries ) => { + const observer = new IntersectionObserver( function ( entries ) { let /** @type {IntersectionObserverEntry | undefined} */ closestNegativeEntry; let /** @type {IntersectionObserverEntry | undefined} */ closestPositiveEntry; const topMargin = /** @type {number} */ ( props.topMargin ); - entries.forEach( ( entry ) => { + entries.forEach( function ( entry ) { const top = entry.boundingClientRect.top - topMargin; if ( top > 0 && - ( closestPositiveEntry === undefined || - top < closestPositiveEntry.boundingClientRect.top - topMargin ) ) { + ( closestPositiveEntry === undefined || + top < closestPositiveEntry.boundingClientRect.top - topMargin ) + ) { closestPositiveEntry = entry; } if ( top <= 0 && - ( closestNegativeEntry === undefined || - top > closestNegativeEntry.boundingClientRect.top - topMargin ) ) { + ( closestNegativeEntry === undefined || + top > closestNegativeEntry.boundingClientRect.top - topMargin ) + ) { closestNegativeEntry = entry; } } ); @@ -96,67 +98,67 @@ const sectionObserver = ( props ) => { observer.disconnect(); } ); - const calcIntersection = () => { + function calcIntersection() { // IntersectionObserver will asynchronously calculate the boundingClientRect // of each observed element off the main thread after `observe` is called. - props.elements.forEach( ( element ) => { + props.elements.forEach( function ( element ) { observer.observe( /** @type {HTMLElement} */( element ) ); } ); - }; + } - const handleScroll = () => { + function handleScroll() { // Throttle the scroll event handler to fire at a rate limited by `props.throttleMs`. if ( !inThrottle ) { inThrottle = true; - setTimeout( () => { + setTimeout( function () { calcIntersection(); inThrottle = false; }, props.throttleMs ); } - }; + } - const bindScrollListener = () => { + function bindScrollListener() { window.addEventListener( 'scroll', handleScroll ); - }; + } - const unbindScrollListener = () => { + function unbindScrollListener() { window.removeEventListener( 'scroll', handleScroll ); - }; + } /** * Pauses intersection observation until `resume` is called. */ - const pause = () => { + function pause() { unbindScrollListener(); // Assume current is no longer valid while paused. current = undefined; - }; + } /** * Resumes intersection observation. */ - const resume = () => { + function resume() { bindScrollListener(); - }; + } /** * Cleans up event listeners and intersection observer. Should be called when * the observer is permanently no longer needed. */ - const unmount = () => { + function unmount() { unbindScrollListener(); observer.disconnect(); - }; + } /** * Set a list of HTML elements to observe for intersection changes. * * @param {NodeList} list */ - const setElements = ( list ) => { + function setElements( list ) { props.elements = list; - }; + } bindScrollListener(); // Calculate intersection on page load. @@ -175,7 +177,7 @@ const sectionObserver = ( props ) => { unmount, setElements }; -}; +} module.exports = { init: sectionObserver diff --git a/resources/skins.citizen.scripts/sections.js b/resources/skins.citizen.scripts/sections.js index aa3198b9..856ceda2 100644 --- a/resources/skins.citizen.scripts/sections.js +++ b/resources/skins.citizen.scripts/sections.js @@ -1,11 +1,14 @@ /** * @return {void} */ -const initCollapsibleSections = () => { - const prefix = 'section-', headings = document.querySelectorAll( '.' + prefix + 'heading' ), sections = document.querySelectorAll( '.' + prefix + 'collapsible' ); +function initCollapsibleSections() { + const prefix = 'section-', + headings = document.querySelectorAll( '.' + prefix + 'heading' ), + sections = document.querySelectorAll( '.' + prefix + 'collapsible' ); for ( let i = 0; i < headings.length; i++ ) { - const j = i + 1, collapsibleID = prefix + 'collapsible-' + j, + const j = i + 1, + collapsibleID = prefix + 'collapsible-' + j, /* T13555 */ headline = headings[ i ].querySelector( '.mw-headline' ) ? headings[ i ].querySelector( '.mw-headline' ) : headings[ i ].querySelector( '.mw-heading' ); @@ -18,13 +21,15 @@ const initCollapsibleSections = () => { // TODO: Need a keyboard handler headings[ i ].addEventListener( 'click', function () { // .section-heading--collapsed + this.classList.toggle( prefix + 'heading--collapsed' ); // .section-collapsible--collapsed + sections[ j ].classList.toggle( prefix + 'collapsible--collapsed' ); headline.setAttribute( 'aria-expanded', headline.getAttribute( 'aria-expanded' ) === 'true' ? 'false' : 'true' ); } ); } -}; +} module.exports = { init: initCollapsibleSections diff --git a/resources/skins.citizen.scripts/skin.js b/resources/skins.citizen.scripts/skin.js index 85770670..524edc10 100644 --- a/resources/skins.citizen.scripts/skin.js +++ b/resources/skins.citizen.scripts/skin.js @@ -1,8 +1,9 @@ -const checkboxHack = require( './checkboxHack.js' ); -const CHECKBOX_HACK_CONTAINER_SELECTOR = '.mw-checkbox-hack-container'; -const CHECKBOX_HACK_CHECKBOX_SELECTOR = '.mw-checkbox-hack-checkbox'; -const CHECKBOX_HACK_BUTTON_SELECTOR = '.mw-checkbox-hack-button'; -const CHECKBOX_HACK_TARGET_SELECTOR = '.mw-checkbox-hack-target'; +const + checkboxHack = require( './checkboxHack.js' ), + CHECKBOX_HACK_CONTAINER_SELECTOR = '.mw-checkbox-hack-container', + CHECKBOX_HACK_CHECKBOX_SELECTOR = '.mw-checkbox-hack-checkbox', + CHECKBOX_HACK_BUTTON_SELECTOR = '.mw-checkbox-hack-button', + CHECKBOX_HACK_TARGET_SELECTOR = '.mw-checkbox-hack-target'; /** * Wait for first paint before calling this function. @@ -11,9 +12,9 @@ const CHECKBOX_HACK_TARGET_SELECTOR = '.mw-checkbox-hack-target'; * @param {Document} document * @return {void} */ -const enableCssAnimations = ( { documentElement } ) => { - documentElement.classList.add( 'citizen-animations-ready' ); -}; +function enableCssAnimations( document ) { + document.documentElement.classList.add( 'citizen-animations-ready' ); +} /** * Add the ability for users to toggle dropdown menus using the enter key (as @@ -23,16 +24,14 @@ const enableCssAnimations = ( { documentElement } ) => { * * @return {void} */ -const bind = () => { +function bind() { // Search for all dropdown containers using the CHECKBOX_HACK_CONTAINER_SELECTOR. - const containers = document.querySelectorAll( - CHECKBOX_HACK_CONTAINER_SELECTOR - ); + const containers = document.querySelectorAll( CHECKBOX_HACK_CONTAINER_SELECTOR ); - containers.forEach( ( container ) => { - const checkbox = container.querySelector( CHECKBOX_HACK_CHECKBOX_SELECTOR ); - const button = container.querySelector( CHECKBOX_HACK_BUTTON_SELECTOR ); - const target = container.querySelector( CHECKBOX_HACK_TARGET_SELECTOR ); + containers.forEach( function ( container ) { + const checkbox = container.querySelector( CHECKBOX_HACK_CHECKBOX_SELECTOR ), + button = container.querySelector( CHECKBOX_HACK_BUTTON_SELECTOR ), + target = container.querySelector( CHECKBOX_HACK_TARGET_SELECTOR ); if ( !( checkbox && button && target ) ) { return; @@ -40,22 +39,20 @@ const bind = () => { checkboxHack.bind( window, checkbox, button, target ); } ); -}; +} /** * Close all menus through unchecking all checkbox hacks * * @return {void} */ -const uncheckCheckboxHacks = () => { - const checkboxes = document.querySelectorAll( - `${CHECKBOX_HACK_CHECKBOX_SELECTOR}:checked` - ); +function uncheckCheckboxHacks() { + const checkboxes = document.querySelectorAll( CHECKBOX_HACK_CHECKBOX_SELECTOR + ':checked' ); - checkboxes.forEach( ( checkbox ) => { + checkboxes.forEach( function ( checkbox ) { /** @type {HTMLInputElement} */ ( checkbox ).checked = false; } ); -}; +} /** * Add a class to indicate that sticky header is active @@ -63,35 +60,45 @@ const uncheckCheckboxHacks = () => { * @param {Document} document * @return {void} */ -const initStickyHeader = ( document ) => { +function initStickyHeader( document ) { const scrollObserver = require( './scrollObserver.js' ); - const sentinel = document.getElementById( - 'citizen-body-header-sticky-sentinel' - ); + // Detect scroll direction and add the right class + // scrollObserver.initDirectionObserver( + // function () { + // document.body.classList.remove( 'citizen-scroll--up' ); + // document.body.classList.add( 'citizen-scroll--down' ); + // }, + // function () { + // document.body.classList.remove( 'citizen-scroll--down' ); + // document.body.classList.add( 'citizen-scroll--up' ); + // }, + // 10 + // ); + + const sentinel = document.getElementById( 'citizen-body-header-sticky-sentinel' ); // In some pages we use display:none to disable the sticky header // Do not start observer if it is set to display:none - if ( sentinel && - getComputedStyle( sentinel ).getPropertyValue( 'display' ) !== 'none' ) { + if ( sentinel && getComputedStyle( sentinel ).getPropertyValue( 'display' ) !== 'none' ) { const observer = scrollObserver.initIntersectionObserver( - () => { + function () { document.body.classList.add( 'citizen-body-header--sticky' ); }, - () => { + function () { document.body.classList.remove( 'citizen-body-header--sticky' ); } ); observer.observe( sentinel ); } -}; +} /** * @param {Window} window * @return {void} */ -const main = ( window ) => { +function main( window ) { const search = require( './search.js' ); enableCssAnimations( window.document ); @@ -114,26 +121,19 @@ const main = ( window ) => { sections.init(); } - window.addEventListener( - 'beforeunload', - () => { - // T295085: Close all dropdown menus when page is unloaded to prevent them - // from being open when navigating back to a page. - uncheckCheckboxHacks(); - // Set up loading indicator - document.documentElement.classList.add( 'citizen-loading' ); - }, - false - ); -}; - -if ( - document.readyState === 'interactive' || - document.readyState === 'complete' -) { + window.addEventListener( 'beforeunload', function () { + // T295085: Close all dropdown menus when page is unloaded to prevent them + // from being open when navigating back to a page. + uncheckCheckboxHacks(); + // Set up loading indicator + document.documentElement.classList.add( 'citizen-loading' ); + }, false ); +} + +if ( document.readyState === 'interactive' || document.readyState === 'complete' ) { main( window ); } else { - document.addEventListener( 'DOMContentLoaded', () => { + document.addEventListener( 'DOMContentLoaded', function () { main( window ); } ); } diff --git a/resources/skins.citizen.scripts/tableOfContents.js b/resources/skins.citizen.scripts/tableOfContents.js index 2abebe8f..d2c6531e 100644 --- a/resources/skins.citizen.scripts/tableOfContents.js +++ b/resources/skins.citizen.scripts/tableOfContents.js @@ -5,10 +5,10 @@ let /** @type {HTMLElement | undefined} */ activeSection; /** * @param {string} id */ -const changeActiveSection = ( id ) => { +function changeActiveSection( id ) { const toc = document.getElementById( 'mw-panel-toc' ); - const getLink = ( hash ) => { + const getLink = function ( hash ) { const prefix = 'a[href="#', suffix = '"]'; let el = toc.querySelector( prefix + hash + suffix ); @@ -30,7 +30,7 @@ const changeActiveSection = ( id ) => { activeSection = link.parentNode; activeSection.classList.add( ACTIVE_SECTION_CLASS ); -}; +} /** * Toggle active HTML class to items in table of content based on user viewport. @@ -38,16 +38,18 @@ const changeActiveSection = ( id ) => { * * @return {void} */ -const initToC = () => { +function initToC() { const bodyContent = document.getElementById( 'bodyContent' ); // We use scroll-padding-top to handle scrolling with fixed header // It is better to respect that so it is consistent - const getTopMargin = () => Number( - window.getComputedStyle( document.documentElement ) - .getPropertyValue( 'scroll-padding-top' ) - .slice( 0, -2 ) - ) + 20; + const getTopMargin = function () { + return Number( + window.getComputedStyle( document.documentElement ) + .getPropertyValue( 'scroll-padding-top' ) + .slice( 0, -2 ) + ) + 20; + }; const initSectionObserver = require( './sectionObserver.js' ).init; @@ -55,14 +57,14 @@ const initToC = () => { /* T13555 */ elements: bodyContent.querySelectorAll( '.mw-headline' ) ? bodyContent.querySelectorAll( '.mw-headline' ) : bodyContent.querySelectorAll( '.mw-heading' ), topMargin: getTopMargin(), - onIntersection: ( section ) => { + onIntersection: function ( section ) { changeActiveSection( section.id ); } } ); // TODO: Pause section observer on ToC link click sectionObserver.resume(); -}; +} module.exports = { init: initToC diff --git a/resources/skins.citizen.search/gateway/gateway.js b/resources/skins.citizen.search/gateway/gateway.js index 54fa830e..24c8eacf 100644 --- a/resources/skins.citizen.search/gateway/gateway.js +++ b/resources/skins.citizen.search/gateway/gateway.js @@ -15,7 +15,7 @@ const gatewayConfig = require( '../config.json' ).wgCitizenSearchGateway; * * @return {module} */ -const getGateway = () => { +function getGateway() { switch ( gatewayConfig ) { case 'mwActionApi': return require( './mwActionApi.js' ); @@ -24,7 +24,7 @@ const getGateway = () => { default: throw new Error( 'Unknown search gateway' ); } -}; +} /** * Fetch suggestion from gateway and return the results object @@ -34,7 +34,7 @@ const getGateway = () => { * @return {Object} Results */ // eslint-disable-next-line es-x/no-async-functions -const getResults = async ( searchQuery, controller ) => { +async function getResults( searchQuery, controller ) { const gateway = getGateway(); const signal = controller.signal; @@ -48,7 +48,7 @@ const getResults = async ( searchQuery, controller ) => { const data = await response.json(); return gateway.convertDataToResults( data ); -}; +} module.exports = { getResults: getResults diff --git a/resources/skins.citizen.search/gateway/mwActionApi.js b/resources/skins.citizen.search/gateway/mwActionApi.js index b9f44bf3..19181a4e 100644 --- a/resources/skins.citizen.search/gateway/mwActionApi.js +++ b/resources/skins.citizen.search/gateway/mwActionApi.js @@ -9,22 +9,25 @@ const config = require( '../config.json' ), * @param {string} input * @return {string} url */ -const getUrl = ( input ) => { - const endpoint = config.wgScriptPath + '/api.php?format=json', cacheExpiry = config.wgSearchSuggestCacheExpiry, maxResults = config.wgCitizenMaxSearchResults, query = { - action: 'query', - smaxage: cacheExpiry, - maxage: cacheExpiry, - generator: 'prefixsearch', - prop: 'pageprops|pageimages', - redirects: '', - ppprop: 'displaytitle', - piprop: 'thumbnail', - pithumbsize: 200, - pilimit: maxResults, - gpssearch: input, - gpsnamespace: 0, - gpslimit: maxResults - }; +function getUrl( input ) { + const endpoint = config.wgScriptPath + '/api.php?format=json', + cacheExpiry = config.wgSearchSuggestCacheExpiry, + maxResults = config.wgCitizenMaxSearchResults, + query = { + action: 'query', + smaxage: cacheExpiry, + maxage: cacheExpiry, + generator: 'prefixsearch', + prop: 'pageprops|pageimages', + redirects: '', + ppprop: 'displaytitle', + piprop: 'thumbnail', + pithumbsize: 200, + pilimit: maxResults, + gpssearch: input, + gpsnamespace: 0, + gpslimit: maxResults + }; switch ( descriptionSource ) { case 'wikidata': @@ -49,7 +52,7 @@ const getUrl = ( input ) => { } return endpoint + queryString; -}; +} /** * Map raw response to Results object @@ -57,8 +60,8 @@ const getUrl = ( input ) => { * @param {Object} data * @return {Object} Results */ -const convertDataToResults = ( data ) => { - const getDisplayTitle = ( item ) => { +function convertDataToResults( data ) { + const getDisplayTitle = function ( item ) { if ( item.pageprops && item.pageprops.displaytitle ) { return item.pageprops.displaytitle; } else { @@ -66,7 +69,7 @@ const convertDataToResults = ( data ) => { } }; - const getDescription = ( item ) => { + const getDescription = function ( item ) { switch ( descriptionSource ) { case 'wikidata': return item.description || ''; @@ -87,7 +90,7 @@ const convertDataToResults = ( data ) => { data = Object.values( data.query.pages ); // Sort the data with the index property since it is not in order - data.sort( ( a, b ) => { + data.sort( function ( a, b ) { return a.index - b.index; } ); @@ -104,7 +107,7 @@ const convertDataToResults = ( data ) => { } return results; -}; +} module.exports = { getUrl: getUrl, diff --git a/resources/skins.citizen.search/gateway/mwRestApi.js b/resources/skins.citizen.search/gateway/mwRestApi.js index 15f1e5ec..93eef49e 100644 --- a/resources/skins.citizen.search/gateway/mwRestApi.js +++ b/resources/skins.citizen.search/gateway/mwRestApi.js @@ -6,11 +6,12 @@ const config = require( '../config.json' ); * @param {string} input * @return {string} url */ -const getUrl = ( input ) => { - const endpoint = config.wgScriptPath + '/rest.php/v1/search/title?q=', query = '&limit=' + config.wgCitizenMaxSearchResults; +function getUrl( input ) { + const endpoint = config.wgScriptPath + '/rest.php/v1/search/title?q=', + query = '&limit=' + config.wgCitizenMaxSearchResults; return endpoint + input + query; -}; +} /** * Map raw response to Results object @@ -18,7 +19,7 @@ const getUrl = ( input ) => { * @param {Object} data * @return {Object} Results */ -const convertDataToResults = ( data ) => { +function convertDataToResults( data ) { const results = []; // eslint-disable-next-line es-x/no-optional-chaining, es-x/no-nullish-coalescing-operators @@ -44,7 +45,7 @@ const convertDataToResults = ( data ) => { } return results; -}; +} module.exports = { getUrl: getUrl, diff --git a/resources/skins.citizen.search/main.js b/resources/skins.citizen.search/main.js index 8bb8c0fb..cc92c0db 100644 --- a/resources/skins.citizen.search/main.js +++ b/resources/skins.citizen.search/main.js @@ -1,13 +1,14 @@ /** * @return {void} */ -const initSearchLoader = () => { - const searchForm = document.getElementById( 'searchform' ), searchInput = document.getElementById( 'searchInput' ); +function initSearchLoader() { + const searchForm = document.getElementById( 'searchform' ), + searchInput = document.getElementById( 'searchInput' ); if ( searchForm && searchInput ) { const typeahead = require( './typeahead.js' ); typeahead.init( searchForm, searchInput ); } -}; +} initSearchLoader(); diff --git a/resources/skins.citizen.search/typeahead.js b/resources/skins.citizen.search/typeahead.js index c8fc15d4..2a089513 100644 --- a/resources/skins.citizen.search/typeahead.js +++ b/resources/skins.citizen.search/typeahead.js @@ -3,13 +3,13 @@ if ( !Array.prototype.includes ) { // eslint-disable-next-line no-extend-native Array.prototype.includes = function ( searchElement, fromIndex ) { - // eslint-disable-next-line unicorn/prefer-includes return this.indexOf( searchElement, fromIndex ) > -1; }; } /* eslint-disable es-x/no-symbol-prototype-description */ -const PREFIX = 'citizen-typeahead', +const + PREFIX = 'citizen-typeahead', SEARCH_LOADING_CLASS = 'citizen-loading', ITEM_CLASS = `${PREFIX}__item`, ACTIVE_CLASS = `${ITEM_CLASS}--active`, @@ -67,7 +67,7 @@ let /** @type {HTMLElement | undefined} */ searchInput; * * @param {HTMLElement} element */ -const toggleActive = ( element ) => { +function toggleActive( element ) { const typeaheadItems = typeahead.querySelectorAll( `.${ITEM_CLASS}` ); for ( let i = 0; i < typeaheadItems.length; i++ ) { @@ -83,7 +83,7 @@ const toggleActive = ( element ) => { } } } -}; +} /** * Keyboard events: up arrow, down arrow and enter. @@ -91,7 +91,7 @@ const toggleActive = ( element ) => { * * @param {Event} event */ -const keyboardEvents = ( event ) => { +function keyboardEvents( event ) { if ( event.defaultPrevented ) { return; // Do nothing if the event was already processed } @@ -107,45 +107,46 @@ const keyboardEvents = ( event ) => { } toggleActive( typeaheadItems[ activeIndex.index ] ); + } if ( typeaheadItems[ activeIndex.index ] ) { - const link = typeaheadItems[ activeIndex.index ].querySelector( - `.${PREFIX}__content` - ); + const link = typeaheadItems[ activeIndex.index ].querySelector( `.${PREFIX}__content` ); if ( event.key === 'Enter' && link instanceof HTMLAnchorElement ) { event.preventDefault(); link.click(); } } -}; +} /** * Bind mouseenter and mouseleave event to reproduce mouse hover event * * @param {HTMLElement} element */ -const bindMouseHoverEvent = ( element ) => { - element.addEventListener( 'mouseenter', ( event ) => { +function bindMouseHoverEvent( element ) { + element.addEventListener( 'mouseenter', function ( event ) { toggleActive( event.currentTarget ); } ); - element.addEventListener( 'mouseleave', ( event ) => { + element.addEventListener( 'mouseleave', function ( event ) { toggleActive( event.currentTarget ); } ); -}; +} /** * Remove all existing suggestions from typeahead */ -const clearSuggestions = () => { +function clearSuggestions() { const typeaheadItems = typeahead.children; if ( typeaheadItems.length > 0 ) { // Do all the work in document fragment then replace the whole list // It is more performant this way - const fragment = new DocumentFragment(), template = document.getElementById( `${PREFIX}-template` ); + const + fragment = new DocumentFragment(), + template = document.getElementById( `${PREFIX}-template` ); - [ ...typeaheadItems ].forEach( ( item ) => { + [ ...typeaheadItems ].forEach( function ( item ) { if ( !item.classList.contains( `${ITEM_CLASS}--page` ) ) { fragment.append( item ); } @@ -158,70 +159,7 @@ const clearSuggestions = () => { searchInput.parentNode.classList.remove( SEARCH_LOADING_CLASS ); searchInput.setAttribute( 'aria-activedescendant', '' ); activeIndex.clear(); -}; - -/** - * Update menu item element - * - * @param {HTMLElement} item - * @param {MenuItemData} data - */ -const updateMenuItem = ( item, data ) => { - if ( data.id ) { - item.setAttribute( 'id', data.id ); - } - if ( data.type ) { - item.classList.add( `${ITEM_CLASS}--${data.type}` ); - } - if ( data.link ) { - const link = item.querySelector( `.${PREFIX}__content` ); - link.setAttribute( 'href', data.link ); - } - if ( data.icon || data.thumbnail ) { - const thumbnail = item.querySelector( `.${PREFIX}__thumbnail` ); - if ( data.thumbnail ) { - thumbnail.style.backgroundImage = `url('${data.thumbnail}')`; - } else { - thumbnail.classList.add( - `${PREFIX}__thumbnail`, - 'citizen-ui-icon', - `mw-ui-icon-wikimedia-${data.icon}` - ); - } - } - if ( data.title ) { - const title = item.querySelector( `.${PREFIX}__title` ); - title.innerHTML = data.title; - } - if ( data.label ) { - const label = item.querySelector( `.${PREFIX}__label` ); - label.innerHTML = data.label; - } - if ( data.description ) { - const description = item.querySelector( `.${PREFIX}__description` ); - description.innerHTML = data.description; - } -}; - -/** - * Generate menu item HTML using the existing template - * - * @param {MenuItemData} data - * @return {HTMLElement|void} - */ -const getMenuItem = ( data ) => { - const template = document.getElementById( `${PREFIX}-template` ); - - // Shouldn't happen but just to be safe - if ( !( template instanceof HTMLTemplateElement ) ) { - return; - } - - const fragment = template.content.cloneNode( true ), item = fragment.querySelector( `.${ITEM_CLASS}` ); - updateMenuItem( item, data ); - bindMouseHoverEvent( item ); - return fragment; -}; +} /** * Fetch suggestions from API and render the suggetions in HTML @@ -230,26 +168,21 @@ const getMenuItem = ( data ) => { * @param {string} htmlSafeSearchQuery * @param {HTMLElement} placeholder */ -const getSuggestions = ( searchQuery, htmlSafeSearchQuery, placeholder ) => { - const renderSuggestions = ( results ) => { +function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) { + function renderSuggestions( results ) { if ( results.length > 0 ) { - const fragment = document.createDocumentFragment(), suggestionLinkPrefix = `${config.wgScriptPath}/index.php?title=Special:Search&search=`; + const + fragment = document.createDocumentFragment(), suggestionLinkPrefix = `${config.wgScriptPath}/index.php?title=Special:Search&search=`; /** * Return the redirect title with search query highlight * * @param {string} text * @return {string} */ - const highlightTitle = ( text ) => { + const highlightTitle = function ( text ) { // eslint-disable-next-line security/detect-non-literal-regexp - const regex = new RegExp( - mw.util.escapeRegExp( htmlSafeSearchQuery ), - 'i' - ); - return text.replace( - regex, - `$&` - ); + const regex = new RegExp( mw.util.escapeRegExp( htmlSafeSearchQuery ), 'i' ); + return text.replace( regex, `$&` ); }; /** * Return the HTML of the redirect label @@ -258,15 +191,17 @@ const getSuggestions = ( searchQuery, htmlSafeSearchQuery, placeholder ) => { * @param {string} matchedTitle * @return {string} */ - const getRedirectLabel = ( title, matchedTitle ) => { + const getRedirectLabel = function ( title, matchedTitle ) { /** * Check if the redirect is useful (T303013) * * @return {boolean} */ - const isRedirectUseful = () => { + const isRedirectUseful = function () { // Change to lowercase then remove space and dashes - const cleanup = ( text ) => text.toLowerCase().replace( /-|\s/g, '' ); + const cleanup = function ( text ) { + return text.toLowerCase().replace( /-|\s/g, '' ); + }; const cleanTitle = cleanup( title ), cleanMatchedTitle = cleanup( matchedTitle ); @@ -280,9 +215,7 @@ const getSuggestions = ( searchQuery, htmlSafeSearchQuery, placeholder ) => { // Result is a redirect // Show the redirect title and highlight it if ( matchedTitle && isRedirectUseful() ) { - html = `
+ html = `
${highlightTitle( matchedTitle )}
`; @@ -292,7 +225,7 @@ const getSuggestions = ( searchQuery, htmlSafeSearchQuery, placeholder ) => { }; // Create suggestion items - results.forEach( ( result, index ) => { + results.forEach( function ( result, index ) { const data = { id: `${PREFIX}-suggestion-${index}`, type: 'page', @@ -317,67 +250,140 @@ const getSuggestions = ( searchQuery, htmlSafeSearchQuery, placeholder ) => { typeahead.prepend( fragment ); } else { // Update placeholder with no result content - updateMenuItem( placeholder, { - icon: 'articleNotFound', - type: 'placeholder', - title: mw - .message( 'citizen-search-noresults-title', htmlSafeSearchQuery ) - .text(), - description: mw.message( 'citizen-search-noresults-desc' ).text() - } ); + updateMenuItem( + placeholder, + { + icon: 'articleNotFound', + type: 'placeholder', + title: mw.message( 'citizen-search-noresults-title', htmlSafeSearchQuery ).text(), + description: mw.message( 'citizen-search-noresults-desc' ).text() + } + ); placeholder.classList.remove( HIDDEN_CLASS ); } - }; + } // Add loading animation searchInput.parentNode.classList.add( SEARCH_LOADING_CLASS ); - const controller = new AbortController(), - abortFetch = () => { + const + // eslint-disable-next-line compat/compat + controller = new AbortController(), + abortFetch = function () { controller.abort(); }; - const gateway = require( './gateway/gateway.js' ), getResults = gateway.getResults( searchQuery, controller ); + const + gateway = require( './gateway/gateway.js' ), + getResults = gateway.getResults( searchQuery, controller ); // Abort fetch if the input is detected // So that fetch request won't be queued up searchInput.addEventListener( 'input', abortFetch, { once: true } ); - getResults - .then( ( results ) => { - searchInput.removeEventListener( 'input', abortFetch ); - clearSuggestions(); - if ( results !== null ) { - renderSuggestions( results ); - } - } ) - .catch( ( error ) => { - searchInput.removeEventListener( 'input', abortFetch ); - searchInput.parentNode.classList.remove( SEARCH_LOADING_CLASS ); - // User can trigger the abort when the fetch event is pending - // There is no need for an error - if ( error.name !== 'AbortError' ) { - const message = `Uh oh, a wild error appears! ${error}`; - throw new Error( message ); - } - } ); -}; + getResults.then( function ( results ) { + searchInput.removeEventListener( 'input', abortFetch ); + clearSuggestions(); + if ( results !== null ) { + renderSuggestions( results ); + } + } ).catch( function ( error ) { + searchInput.removeEventListener( 'input', abortFetch ); + searchInput.parentNode.classList.remove( SEARCH_LOADING_CLASS ); + // User can trigger the abort when the fetch event is pending + // There is no need for an error + if ( error.name !== 'AbortError' ) { + const message = `Uh oh, a wild error appears! ${error}`; + throw new Error( message ); + } + } ); +} + +/** + * Update menu item element + * + * @param {HTMLElement} item + * @param {MenuItemData} data + */ +function updateMenuItem( item, data ) { + if ( data.id ) { + item.setAttribute( 'id', data.id ); + } + if ( data.type ) { + item.classList.add( `${ITEM_CLASS}--${data.type}` ); + } + if ( data.link ) { + const link = item.querySelector( `.${PREFIX}__content` ); + link.setAttribute( 'href', data.link ); + } + if ( data.icon || data.thumbnail ) { + const thumbnail = item.querySelector( `.${PREFIX}__thumbnail` ); + if ( data.thumbnail ) { + thumbnail.style.backgroundImage = `url('${data.thumbnail}')`; + } else { + thumbnail.classList.add( + `${PREFIX}__thumbnail`, + 'citizen-ui-icon', + `mw-ui-icon-wikimedia-${data.icon}` + ); + } + } + if ( data.title ) { + const title = item.querySelector( `.${PREFIX}__title` ); + title.innerHTML = data.title; + } + if ( data.label ) { + const label = item.querySelector( `.${PREFIX}__label` ); + label.innerHTML = data.label; + } + if ( data.description ) { + const description = item.querySelector( `.${PREFIX}__description` ); + description.innerHTML = data.description; + } +} + +/** + * Generate menu item HTML using the existing template + * + * @param {MenuItemData} data + * @return {HTMLElement|void} + */ +function getMenuItem( data ) { + const template = document.getElementById( `${PREFIX}-template` ); + + // Shouldn't happen but just to be safe + if ( !( template instanceof HTMLTemplateElement ) ) { + return; + } + + const + fragment = template.content.cloneNode( true ), + item = fragment.querySelector( `.${ITEM_CLASS}` ); + updateMenuItem( item, data ); + bindMouseHoverEvent( item ); + return fragment; +} /** * Update the typeahead element * * @param {Object} messages */ -const updateTypeahead = ( messages ) => { - const searchQuery = searchInput.value, htmlSafeSearchQuery = mw.html.escape( searchQuery ), hasQuery = searchQuery.length > 0, placeholder = typeahead.querySelector( `.${ITEM_CLASS}--placeholder` ); +function updateTypeahead( messages ) { + const + searchQuery = searchInput.value, + htmlSafeSearchQuery = mw.html.escape( searchQuery ), + hasQuery = searchQuery.length > 0, + placeholder = typeahead.querySelector( `.${ITEM_CLASS}--placeholder` ); /** * Update a tool item or create it if it does not exist * * @param {Object} data */ - const updateToolItem = ( data ) => { - const itemId = `${PREFIX}-${data.id}`, query = `${htmlSafeSearchQuery}`, itemLink = data.link + searchQuery, + const updateToolItem = function ( data ) { + const + itemId = `${PREFIX}-${data.id}`, query = `${htmlSafeSearchQuery}`, itemLink = data.link + searchQuery, /* eslint-disable-next-line mediawiki/msg-doc */ itemDesc = mw.message( data.msg, query ); @@ -386,10 +392,13 @@ const updateTypeahead = ( messages ) => { // Update existing element instead of creating a new one if ( item ) { // FIXME: Probably more efficient to just replace the query than the whole messaage? - updateMenuItem( item, { - link: itemLink, - description: itemDesc - } ); + updateMenuItem( + item, + { + link: itemLink, + description: itemDesc + } + ); // FIXME: There is probably a more efficient way if ( hasQuery ) { item.classList.remove( HIDDEN_CLASS ); @@ -431,42 +440,48 @@ const updateTypeahead = ( messages ) => { } else { clearSuggestions(); // Update placeholder with no query content - updateMenuItem( placeholder, { - icon: 'articlesSearch', - title: messages.emptyTitle, - description: messages.emptyDesc - } ); + updateMenuItem( + placeholder, + { + icon: 'articlesSearch', + title: messages.emptyTitle, + description: messages.emptyDesc + } + ); placeholder.classList.remove( HIDDEN_CLASS ); } // -1 as there is a template element activeIndex.setMax( typeahead.children.length - 1 ); -}; +} /** * @param {HTMLElement} searchForm * @param {HTMLInputElement} input */ -const initTypeahead = ( searchForm, input ) => { +function initTypeahead( searchForm, input ) { const EXPANDED_CLASS = 'citizen-typeahead--expanded'; - const messages = { + const + messages = { emptyTitle: mw.message( 'searchsuggest-search' ).text(), emptyDesc: mw.message( 'citizen-search-empty-desc' ).text() - }, template = mw.template.get( + }, + template = mw.template.get( 'skins.citizen.search', 'resources/skins.citizen.search/templates/typeahead.mustache' - ), data = { + ), + data = { 'msg-searchsuggest-search': messages.emptyTitle, 'msg-citizen-search-empty-desc': messages.emptyDesc }; - const onBlur = ( event ) => { + const onBlur = function ( event ) { const focusIn = typeahead.contains( event.relatedTarget ); if ( !focusIn ) { // HACK: On Safari, users are unable to click any links because the blur // event dismiss the links before it is clicked. This should fix it. - setTimeout( () => { + setTimeout( function () { searchInput.setAttribute( 'aria-activedescendant', '' ); typeahead.classList.remove( EXPANDED_CLASS ); searchInput.removeEventListener( 'keydown', keyboardEvents ); @@ -475,7 +490,7 @@ const initTypeahead = ( searchForm, input ) => { } }; - const onFocus = () => { + const onFocus = function () { // Refresh the typeahead since the query will be emptied when blurred updateTypeahead( messages ); typeahead.classList.add( EXPANDED_CLASS ); @@ -501,10 +516,11 @@ const initTypeahead = ( searchForm, input ) => { updateTypeahead( messages ); } - searchInput.addEventListener( 'input', () => { + searchInput.addEventListener( 'input', function () { mw.util.debounce( 100, updateTypeahead( messages ) ); } ); -}; + +} module.exports = { init: initTypeahead diff --git a/skin.json b/skin.json index e45727e9..29db49e4 100644 --- a/skin.json +++ b/skin.json @@ -97,7 +97,6 @@ ] }, "skins.citizen.scripts": { - "es6": true, "packageFiles": [ "resources/skins.citizen.scripts/skin.js", { @@ -120,7 +119,6 @@ ] }, "skins.citizen.search": { - "es6": true, "templates": [ "resources/skins.citizen.search/templates/typeahead.mustache" ],