resources/src/mediawiki.special.preferences.ooui/tabs.js
/*!
* JavaScript for Special:Preferences: Tab navigation.
*/
( function () {
var nav = require( './nav.js' );
$( function () {
var $tabNavigationHint = nav.insertHints( mw.msg( 'prefs-tabs-navigation-hint' ) );
var tabs = OO.ui.infuse( $( '.mw-prefs-tabs' ) );
// Support: Chrome
// https://bugs.chromium.org/p/chromium/issues/detail?id=1252507
//
// Infusing the tabs above involves detaching all the tabs' content from the DOM momentarily,
// which causes the :target selector (used in mediawiki.special.preferences.styles.ooui.less)
// not to match anything inside the tabs in Chrome. Twiddling location.href makes it work.
// Only do it when a fragment is present, otherwise the page would be reloaded.
if ( location.href.indexOf( '#' ) !== -1 ) {
// eslint-disable-next-line no-self-assign
location.href = location.href;
}
tabs.$element.addClass( 'mw-prefs-tabs-infused' );
function enhancePanel( panel ) {
if ( !panel.$element.data( 'mw-section-infused' ) ) {
panel.$element.removeClass( 'mw-htmlform-autoinfuse-lazy' );
mw.hook( 'htmlform.enhance' ).fire( panel.$element );
panel.$element.data( 'mw-section-infused', true );
}
}
function onTabPanelSet( panel ) {
if ( nav.switchingNoHash ) {
return;
}
// Handle hash manually to prevent jumping,
// therefore save and restore scrollTop to prevent jumping.
var scrollTop = $( window ).scrollTop();
// Changing the hash apparently causes keyboard focus to be lost?
// Save and restore it. This makes no sense though.
var active = document.activeElement;
location.hash = '#' + panel.getName();
if ( active ) {
active.focus();
}
$( window ).scrollTop( scrollTop );
}
tabs.on( 'set', onTabPanelSet );
// Hash navigation callback
var setSection = function ( sectionName, fieldset ) {
tabs.setTabPanel( sectionName );
enhancePanel( tabs.getCurrentTabPanel() );
// Scroll to a fieldset if provided.
if ( fieldset ) {
fieldset.scrollIntoView();
}
};
// onSubmit callback
var onSubmit = function () {
var value = tabs.getCurrentTabPanelName();
mw.storage.session.set( 'mwpreferences-prevTab', value );
};
nav.onLoad( setSection, 'mw-prefsection-personal' );
nav.restorePrevSection( setSection, onSubmit );
// Search index
var index, texts;
function buildIndex() {
index = {};
var $fields = tabs.contentPanel.$element.find( '[class^=mw-htmlform-field-]:not( .mw-prefs-search-noindex )' );
var $descFields = $fields.filter(
'.oo-ui-fieldsetLayout-group > .oo-ui-widget > .mw-htmlform-field-HTMLInfoField'
);
$fields.not( $descFields ).each( function () {
var $field = $( this );
var $wrapper = $field.parents( '.mw-prefs-fieldset-wrapper' );
var $tabPanel = $field.closest( '.oo-ui-tabPanelLayout' );
var $labels = $field.find(
'.oo-ui-labelElement-label, .oo-ui-textInputWidget .oo-ui-inputWidget-input, p'
).add(
$wrapper.find( '> .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header .oo-ui-labelElement-label' )
);
$field = $field.add( $tabPanel.find( $descFields ) );
function addToIndex( $label, $highlight ) {
var text = $label.val() || $label[ 0 ].textContent.toLowerCase().trim().replace( /\s+/, ' ' );
if ( text ) {
index[ text ] = index[ text ] || [];
index[ text ].push( {
$highlight: $highlight || $label,
$field: $field,
$wrapper: $wrapper,
$tabPanel: $tabPanel
} );
}
}
$labels.each( function () {
addToIndex( $( this ) );
// Check if there we are in an infusable dropdown and collect other options
var $dropdown = $( this ).closest( '.oo-ui-dropdownInputWidget[data-ooui],.mw-widget-selectWithInputWidget[data-ooui]' );
if ( $dropdown.length ) {
var dropdown = OO.ui.infuse( $dropdown[ 0 ] );
var dropdownWidget = ( dropdown.dropdowninput || dropdown ).dropdownWidget;
if ( dropdownWidget ) {
dropdownWidget.getMenu().getItems().forEach( function ( option ) {
// Highlight the dropdown handle and the matched label, for when the dropdown is opened
addToIndex( option.$label, dropdownWidget.$handle );
addToIndex( option.$label, option.$label );
} );
}
}
} );
} );
mw.hook( 'prefs.search.buildIndex' ).fire( index );
texts = Object.keys( index );
}
function infuseAllPanels() {
tabs.stackLayout.items.forEach( function ( tabPanel ) {
var wasVisible = tabPanel.isVisible();
// Force panel to be visible while infusing
tabPanel.toggle( true );
enhancePanel( tabPanel );
// Restore visibility
tabPanel.toggle( wasVisible );
} );
}
var searchWrapper = OO.ui.infuse( $( '.mw-prefs-search' ) );
var search = searchWrapper.fieldWidget;
search.$input.on( 'focus', function () {
if ( !index ) {
// Lazy-build index on first focus
// Infuse all widgets as we may end up showing a large subset of them
infuseAllPanels();
buildIndex();
}
} );
var $noResults = $( '<div>' ).addClass( 'mw-prefs-noresults' ).text( mw.msg( 'searchprefs-noresults' ) );
search.on( 'change', function ( val ) {
if ( !index ) {
// In case 'focus' hasn't fired yet
infuseAllPanels();
buildIndex();
}
var isSearching = !!val;
tabs.$element.toggleClass( 'mw-prefs-tabs-searching', isSearching );
tabs.tabSelectWidget.toggle( !isSearching );
tabs.contentPanel.setContinuous( isSearching );
$( '.mw-prefs-search-matched' ).removeClass( 'mw-prefs-search-matched' );
$( '.mw-prefs-search-highlight' ).removeClass( 'mw-prefs-search-highlight' );
var countResults = 0;
if ( isSearching ) {
val = val.toLowerCase();
texts.forEach( function ( text ) {
// TODO: Could use Intl.Collator.prototype.compare like OO.ui.mixin.LabelElement.static.highlightQuery
// but might be too slow.
if ( text.indexOf( val ) !== -1 ) {
index[ text ].forEach( function ( item ) {
// eslint-disable-next-line no-jquery/no-class-state
if ( !item.$field.hasClass( 'mw-prefs-search-matched' ) ) {
// Count each matched preference as one result, not the number of matches in the text
countResults++;
}
item.$highlight.addClass( 'mw-prefs-search-highlight' );
item.$field.addClass( 'mw-prefs-search-matched' );
item.$wrapper.addClass( 'mw-prefs-search-matched' );
item.$tabPanel.addClass( 'mw-prefs-search-matched' );
} );
}
} );
}
// We hide the tabs when searching, so hide this tip about them as well
$tabNavigationHint.toggle( !isSearching );
// Update invisible label to give screenreader users live feedback while they're typing
if ( !isSearching ) {
searchWrapper.setLabel( mw.msg( 'searchprefs' ) );
} else if ( countResults === 0 ) {
searchWrapper.setLabel( mw.msg( 'searchprefs-noresults' ) );
} else {
searchWrapper.setLabel( mw.msg( 'searchprefs-results', countResults ) );
}
// Update visible label
if ( isSearching && countResults === 0 ) {
tabs.$element.append( $noResults );
} else {
$noResults.detach();
}
// Make Enter jump to the results, if there are any
if ( isSearching && countResults !== 0 ) {
search.on( 'enter', function () {
tabs.focusFirstFocusable();
} );
} else {
search.off( 'enter' );
}
} );
// Handle the initial value in case the user started typing before this JS code loaded,
// or the browser restored the value for a closed tab
if ( search.getValue() ) {
search.emit( 'change', search.getValue() );
}
} );
}() );