Merge "rdbms: make transaction rounds apply DBO_TRX to DB_REPLICA connections"
[mediawiki.git] / resources / src / mediawiki.special.preferences.ooui / tabs.js
blobf97ab627d4b91a82938d53264c65ad494182cb4f
1 /*!
2  * JavaScript for Special:Preferences: Tab navigation.
3  */
4 ( function () {
5         const nav = require( './nav.js' );
6         $( () => {
7                 const $tabNavigationHint = nav.insertHints( mw.msg( 'prefs-tabs-navigation-hint' ) );
9                 const tabs = OO.ui.infuse( $( '.mw-prefs-tabs' ) );
11                 // Support: Chrome
12                 // https://bugs.chromium.org/p/chromium/issues/detail?id=1252507
13                 //
14                 // Infusing the tabs above involves detaching all the tabs' content from the DOM momentarily,
15                 // which causes the :target selector (used in mediawiki.special.preferences.styles.ooui.less)
16                 // not to match anything inside the tabs in Chrome. Twiddling location.href makes it work.
17                 // Only do it when a fragment is present, otherwise the page would be reloaded.
18                 if ( location.href.indexOf( '#' ) !== -1 ) {
19                         // eslint-disable-next-line no-self-assign
20                         location.href = location.href;
21                 }
23                 tabs.$element.addClass( 'mw-prefs-tabs-infused' );
25                 function enhancePanel( panel ) {
26                         if ( !panel.$element.data( 'mw-section-infused' ) ) {
27                                 panel.$element.removeClass( 'mw-htmlform-autoinfuse-lazy' );
28                                 mw.hook( 'htmlform.enhance' ).fire( panel.$element );
29                                 panel.$element.data( 'mw-section-infused', true );
30                         }
31                 }
33                 function onTabPanelSet( panel ) {
34                         if ( nav.switchingNoHash ) {
35                                 return;
36                         }
37                         // Handle hash manually to prevent jumping,
38                         // therefore save and restore scrollTop to prevent jumping.
39                         const scrollTop = $( window ).scrollTop();
40                         // Changing the hash apparently causes keyboard focus to be lost?
41                         // Save and restore it. This makes no sense though.
42                         const active = document.activeElement;
43                         location.hash = '#' + panel.getName();
44                         if ( active ) {
45                                 active.focus();
46                         }
47                         $( window ).scrollTop( scrollTop );
48                 }
50                 tabs.on( 'set', onTabPanelSet );
52                 // Hash navigation callback
53                 const setSection = function ( sectionName, fieldset ) {
54                         tabs.setTabPanel( sectionName );
55                         enhancePanel( tabs.getCurrentTabPanel() );
56                         // Scroll to a fieldset if provided.
57                         if ( fieldset ) {
58                                 fieldset.scrollIntoView();
59                         }
60                 };
62                 // onSubmit callback
63                 const onSubmit = function () {
64                         const value = tabs.getCurrentTabPanelName();
65                         mw.storage.session.set( 'mwpreferences-prevTab', value );
66                 };
68                 nav.onLoad( setSection, 'mw-prefsection-personal' );
70                 nav.restorePrevSection( setSection, onSubmit );
72                 // Search index
73                 let index, texts;
74                 function buildIndex() {
75                         index = {};
76                         const $fields = tabs.contentPanel.$element.find( '[class^=mw-htmlform-field-]:not( .mw-prefs-search-noindex )' );
77                         const $descFields = $fields.filter(
78                                 '.oo-ui-fieldsetLayout-group > .oo-ui-widget > .mw-htmlform-field-HTMLInfoField'
79                         );
80                         $fields.not( $descFields ).each( function () {
81                                 let $field = $( this );
82                                 const $wrapper = $field.parents( '.mw-prefs-fieldset-wrapper' );
83                                 const $tabPanel = $field.closest( '.oo-ui-tabPanelLayout' );
84                                 const $labels = $field.find(
85                                         '.oo-ui-labelElement-label, .oo-ui-textInputWidget .oo-ui-inputWidget-input, p'
86                                 ).add(
87                                         $wrapper.find( '> .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header .oo-ui-labelElement-label' )
88                                 );
89                                 $field = $field.add( $tabPanel.find( $descFields ) );
91                                 function addToIndex( $label, $highlight ) {
92                                         const text = $label.val() || $label[ 0 ].textContent.toLowerCase().trim().replace( /\s+/, ' ' );
93                                         if ( text ) {
94                                                 index[ text ] = index[ text ] || [];
95                                                 index[ text ].push( {
96                                                         $highlight: $highlight || $label,
97                                                         $field: $field,
98                                                         $wrapper: $wrapper,
99                                                         $tabPanel: $tabPanel
100                                                 } );
101                                         }
102                                 }
104                                 $labels.each( function () {
105                                         addToIndex( $( this ) );
107                                         // Check if there we are in an infusable dropdown and collect other options
108                                         const $dropdown = $( this ).closest( '.oo-ui-dropdownInputWidget[data-ooui],.mw-widget-selectWithInputWidget[data-ooui]' );
109                                         if ( $dropdown.length ) {
110                                                 const dropdown = OO.ui.infuse( $dropdown[ 0 ] );
111                                                 const dropdownWidget = ( dropdown.dropdowninput || dropdown ).dropdownWidget;
112                                                 if ( dropdownWidget ) {
113                                                         dropdownWidget.getMenu().getItems().forEach( ( option ) => {
114                                                                 // Highlight the dropdown handle and the matched label, for when the dropdown is opened
115                                                                 addToIndex( option.$label, dropdownWidget.$handle );
116                                                                 addToIndex( option.$label, option.$label );
117                                                         } );
118                                                 }
119                                         }
120                                 } );
121                         } );
122                         mw.hook( 'prefs.search.buildIndex' ).fire( index );
123                         texts = Object.keys( index );
124                 }
126                 function infuseAllPanels() {
127                         tabs.stackLayout.items.forEach( ( tabPanel ) => {
128                                 const wasVisible = tabPanel.isVisible();
129                                 // Force panel to be visible while infusing
130                                 tabPanel.toggle( true );
132                                 enhancePanel( tabPanel );
134                                 // Restore visibility
135                                 tabPanel.toggle( wasVisible );
136                         } );
137                 }
139                 const searchWrapper = OO.ui.infuse( $( '.mw-prefs-search' ) );
140                 const search = searchWrapper.fieldWidget;
141                 search.$input.on( 'focus', () => {
142                         if ( !index ) {
143                                 // Lazy-build index on first focus
144                                 // Infuse all widgets as we may end up showing a large subset of them
145                                 infuseAllPanels();
146                                 buildIndex();
147                         }
148                 } );
149                 const $noResults = $( '<div>' ).addClass( 'mw-prefs-noresults' ).text( mw.msg( 'searchprefs-noresults' ) );
150                 search.on( 'change', ( val ) => {
151                         if ( !index ) {
152                                 // In case 'focus' hasn't fired yet
153                                 infuseAllPanels();
154                                 buildIndex();
155                         }
156                         const isSearching = !!val;
157                         tabs.$element.toggleClass( 'mw-prefs-tabs-searching', isSearching );
158                         tabs.tabSelectWidget.toggle( !isSearching );
159                         tabs.contentPanel.setContinuous( isSearching );
161                         $( '.mw-prefs-search-matched' ).removeClass( 'mw-prefs-search-matched' );
162                         $( '.mw-prefs-search-highlight' ).removeClass( 'mw-prefs-search-highlight' );
163                         let countResults = 0;
164                         if ( isSearching ) {
165                                 val = val.toLowerCase();
166                                 texts.forEach( ( text ) => {
167                                         // TODO: Could use Intl.Collator.prototype.compare like OO.ui.mixin.LabelElement.static.highlightQuery
168                                         // but might be too slow.
169                                         if ( text.indexOf( val ) !== -1 ) {
170                                                 index[ text ].forEach( ( item ) => {
171                                                         // eslint-disable-next-line no-jquery/no-class-state
172                                                         if ( !item.$field.hasClass( 'mw-prefs-search-matched' ) ) {
173                                                                 // Count each matched preference as one result, not the number of matches in the text
174                                                                 countResults++;
175                                                         }
176                                                         item.$highlight.addClass( 'mw-prefs-search-highlight' );
177                                                         item.$field.addClass( 'mw-prefs-search-matched' );
178                                                         item.$wrapper.addClass( 'mw-prefs-search-matched' );
179                                                         item.$tabPanel.addClass( 'mw-prefs-search-matched' );
180                                                 } );
181                                         }
182                                 } );
183                         }
185                         // We hide the tabs when searching, so hide this tip about them as well
186                         $tabNavigationHint.toggle( !isSearching );
187                         // Update invisible label to give screenreader users live feedback while they're typing
188                         if ( !isSearching ) {
189                                 searchWrapper.setLabel( mw.msg( 'searchprefs' ) );
190                         } else if ( countResults === 0 ) {
191                                 searchWrapper.setLabel( mw.msg( 'searchprefs-noresults' ) );
192                         } else {
193                                 searchWrapper.setLabel( mw.msg( 'searchprefs-results', countResults ) );
194                         }
196                         // Update visible label
197                         if ( isSearching && countResults === 0 ) {
198                                 tabs.$element.append( $noResults );
199                         } else {
200                                 $noResults.detach();
201                         }
203                         // Make Enter jump to the results, if there are any
204                         if ( isSearching && countResults !== 0 ) {
205                                 search.on( 'enter', () => {
206                                         tabs.focusFirstFocusable();
207                                 } );
208                         } else {
209                                 search.off( 'enter' );
210                         }
212                 } );
214                 // Handle the initial value in case the user started typing before this JS code loaded,
215                 // or the browser restored the value for a closed tab
216                 if ( search.getValue() ) {
217                         search.emit( 'change', search.getValue() );
218                 }
220         } );
221 }() );